I think @JackAidley said the gist of it already, but let me formulate it like this:
without Exceptions (e.g. C)
In regular code flow, you have:
if (condition) {
statement;
} else if (less_likely_condition) {
less_likely_statement;
} else {
least_likely_statement;
}
more_statements;
In the “error out early” case, your code suddenly reads:
/* demonstration example, do NOT code like this */
if (condition) {
statement;
} else {
error_handling;
return;
}
If you spot this pattern – a return
in an else
(or even if
) block, rework it immediately so the code in question does not have an else
block:
/* only code like this at University, to please structured programming professors */
function foo {
if (condition) {
lots_of_statements;
}
return;
}
In the real world…
/* code like this instead */
if (!condition) {
error_handling;
return;
}
lots_of_statements;
This avoids nesting too deep and fulfils the “break out early” case (helps to keep the mind – and the code flow – clean) and doesn’t violate the “put the more likely thing into the if
part” because there simply is no else
part.
C
and cleanup
Inspired by an answer on a similar question (which got this wrong), here's how you do cleanup with C. You can use one or two exit points there, here's one for two exit points:
struct foo *
alloc_and_init(size_t arg1, int arg2)
{
struct foo *res;
if (!(res = calloc(sizeof(struct foo), 1)))
return (NULL);
if (foo_init1(res, arg1))
goto err;
res.arg1_inited = true;
if (foo_init2(&(res->blah), arg2))
goto err;
foo_init_complete(res);
return (res);
err:
/* safe because we use calloc and false == 0 */
if (res.arg1_inited)
foo_dispose1(res);
free(res);
return (NULL);
}
You can collapse them into one exit point if there is less cleanup to do:
char *
NULL_safe_strdup(const char *arg)
{
char *res = NULL;
if (arg == NULL)
goto out;
/* imagine more lines here */
res = strdup(arg);
out:
return (res);
}
This use of goto
is perfectly fine, if you can deal with it; the advice to stay off using goto
is directed at people who cannot yet decide by themselves whether a use is good, acceptable, bad, spaghetti code, or something else.
Exceptions
The above talks about languages without exceptions, which I highly prefer myself (I can use explicit error handling much better, and with much less surprise). To quote igli:
<igli> exceptions: a truly awful implementation of quite a nice idea.
<igli> just about the worst way you could do something like that, afaic.
<igli> it's like anti-design.
<mirabilos> that too… may I quote you on that?
<igli> sure, tho i doubt anyone will listen ;)
But here's a suggestion how you do it well in a language with exceptions, and when you want to use them well:
error return in the face of exceptions
You can replace most of the early return
s with throwing an exception. However, your normal program flow, i.e. any code flow in which the program did not encounter, well, an exception… an error condition or somesuch, shall not raise any exceptions.
This means that…
# this page is only available to logged-in users
if not isLoggedIn():
# this is Python 2.5 style; insert your favourite raise/throw here
raise "eh?"
… is okay, but…
/* do not code like this! */
try {
openFile(xyz, "rw");
} catch (LockedException e) {
return "file is locked";
}
closeFile(xyz);
return "file is not locked";
… is not. Basically, an exception is not a control flow element. This also makes Operations look weird at you (“those Java™ programmers always tell us that these exceptions are normal”) and can hinder debugging (e.g. tell the IDE to just break on any exception). Exceptions often require the run-time environment to unwind the stack to produce tracebacks, etc. There are probably more reasons against it.
This boils down to: in a language supporting exceptions, use whatever matches the existing logic and style and feels natural. If writing something from scratch, get this agreed on early. If writing a library from scratch, think of your consumers. (Do not, ever, use abort()
in a library either…) But whatever you do, do not, as a rule of thumb, have an exception thrown if operation continues (more or less) normally after it.
general advice wrt. Exceptions
Try to get all in-program use of Exceptions agreed on by the whole dev team first. Basically, plan them. Do not use them in abundance. Sometimes, even in C++, Java™, Python, an error return is better. Sometimes it is not; use them with thought.
if
branches returns, use it first. And avoid theelse
for the rest of the code, you've already returned if pre-conditions failed. Code is easier to read, less indentation...