When you have a function or piece of code that must handle several different cases, I find it much better to eliminate special cases using return
or throw
at the beginning. (“returning early”).
if (!input.isValid()) { throw new InvalidInputException(); } // handle input (20 lines)
Several advantages here:
- It frees our mind from worrying about those cases as we read the rest of the function. Programmers, as humans, have limited mental stacks, and if we’re reading a function that must handle several cases, each of those cases must be held in our memory until we see the code that handles them.
- It simplifies the remaining code, since it’s handling a smaller set of cases.
- It compartmentalizes the code into: “handle special cases, then handle the common case.”
- It keeps the code that handles special cases close to the checks for those cases.
- The primary piece of code stays at the root indent level.
- Diff views stay simple when adding/removing special conditions (it does not introduce big indenting changes across many lines).
In fact there’s (what I consider to be) an anti-pattern that involves checking for special cases early, but instead placing the meat of the function in the if
block, followed by an else
block to handle the special case:
if (input.isValid()) { // handle input (20 lines) } else { throw new InvalidInputException(); }
Notice how the handling of the error can now become separated from the cause of the error, and the meat of the code now must be indented. This anti-pattern gets obviously worse the more conditions you must test for:
if (input1.isValid()) { if (input2.isValid()) { if (input3.isValid()) { if (input4.isValid()) { // handle input (20 lines) } else { throw new InvalidInputException('input4 is bad'); } } else { throw new InvalidInputException('input3 is bad'); } } else { throw new InvalidInputException('input2 is bad'); } } else { throw new InvalidInputException('input1 is bad'); }
This code is hard to read, hard to edit, and harder to review when changes occur to the conditions:
- The invalid handling code appears in the reverse order that the inputs are checked.
- The invalid handling code is very far from the conditions (you have to lean on the IDE to see where the braces line up).
- The meat of the function starts off 4 indents over.
- Adding/removing validity checks is a huge pain that involves re-indenting a bunch of code.
- Change diffs can look really messy as indent levels are changed.
Code with a lot of special cases/conditions probably needs to be refactored to separate validation from the processing of the input.
There are certainly situations where returning early is not an obvious win (hence “when reasonable”). This often happens when you have a return value that contains complex state, or some statements must be executed before every return
/throw
statement.