One thing I learned early on in my education was a concept called early exiting.
During the computer science portion of my stay at Lambda School (now BloomTech, don’t ask me why they downgraded names) we learned about early exits when dealing with recursive functions. We would define base cases and it was suggested that these be placed at the top of the function- which would effectively exit the function early.
At the time I didn’t appreciate this little code tip as much as I do now. Partly because my brain was too busy trying (and failing) to mentally emulate the process of recursion for whatever algorithm I was working on at the time.
Early exiting is one of my favorite code conventions these days. It is the process of handling exit cases as soon as possible in a function. For recursive functions this will be your base case(s). In other functions this may be when dealing with inputs that are invalid, unprocessable, or simply don’t need to be further processed.
On the design side of things it helps keep our functions easy to read and avoids complicated if-statements. It also allows us to skip any unnecessary variable instantiation or other function calls if we know we need to exit anyway.
But defining early exit cases is not just a design skill: it bleeds into being a necessary skill for writing robust and predictable software by allowing us to elegantly handle edge cases or invalid inputs.
Let’s look at some examples.
The following example is one you might be familiar with if you’ve ever dipped your toes into recursion. The classic Fibonacci sequence case.
Here, we have a recursive function fibonacci
that returns the Nth Fibonacci value from the Fibonacci sequence as well as the print_fibonacci_values
function which prints out lengthToPrint
values from the Fibonacci sequence.
function fibonacci(num){ if (num < 2) return num // Base case return fibonacci(num - 1) + fibonacci(num - 2) } function print_fibonacci_values(n){ if (!Number.isInteger(n)) return alert("Input must be an integer!") // Early exit - invalid input if (n < 1) return alert("Input must be a positive integer!") // Early exit - invalid input for (let i = 0; i < n; i++){ console.log(fibonacci(i)) } } const lengthToPrint = input("How many Fibonacci values should I print? ") print_fibonacci_values(lengthToPrint)
You can see a few examples of early exits here.
The base case: The base case in the fibonacci
sequence returns the input right away if it’s less than 2. This is because the input needs no further processing - the values in the Fibonacci sequence at indexes 0 and 1 are in fact 0 and 1 respectively.
Invalid input cases: You can also see an example of invalid input cases being handled here in the print_fibonacci_values
function. They respond to the input immediately at the top of the function by returning early with an alert that the input is invalid.
The other benefit of early exiting is writing the cleanest if-statements you’ve ever seen.
Have you ever seen code like this?
const getShippingCost = (shipSpeed) => { let cost = 0; if (shipSpeed === 'standard') { cost += 100; } else if (shipSpeed === 'priority') { cost += 200; } else if (shipSpeed === 'gold'){ cost += 400; } return cost; };
We can blame JavaScript for having some of the ugliest if-statements in the game. Regardless, this is bad. Let’s try throwing in some early returns.
const getShippingCost = (shipSpeed) => { if (shipSpeed === 'standard') return 100; if (shipSpeed === 'priority') return 200; if (shipSpeed === 'gold') return 400; };
Same logic but much more readable.
Early exits also help prevent nested if-statements. For an example of that I encourage you to check out this blog post by Yvonnick Frin.
To demonstrate the use of early exits in the real world let’s take a look at the buildURL
helper function from the Open Source HTTP JS library axios.
/** * Build a URL by appending params to the end * * @param {string} url The base of the url (e.g., http://www.google.com) * @param {object} [params] The params to be appended * @param {?object} options * * @returns {string} The formatted url */ export default function buildURL(url, params, options) { /*eslint no-param-reassign:0*/ if (!params) { return url; } const _encode = options && options.encode || encode; const serializeFn = options && options.serialize; let serializedParams; if (serializeFn) { serializedParams = serializeFn(params, options); } else { serializedParams = utils.isURLSearchParams(params) ? params.toString() : new AxiosURLSearchParams(params, options).toString(_encode); } if (serializedParams) { const hashmarkIndex = url.indexOf("#"); if (hashmarkIndex !== -1) { url = url.slice(0, hashmarkIndex); } url += (url.indexOf('?') === -1 ? '?' : '&') + serializedParams; } return url; }
The function contains a clear exit case right at the top if (!params)
.
No params
object provided? Then there’s no need to further process the URL - just return that bad boy right away.
This is concise and easy for developers to understand. Now why on Earth they opted for a multi-line if-statement instead of a beautiful JS one-liner will puzzle philosophers for centuries, but the library is so incredibly useful we can let it slide for now.
Handling your exits early, especially in languages like JavaScript that allow for single line if-statement returns, is a powerful way to make your code more readable, predictable, and robust.