Early Returns

Clearwater Analytics Engineering
cwan-engineering
Published in
10 min readSep 2, 2022
(“Matrjoschka-Steckfiguren” by Hadiseh Aghdam is licensed under CC-BY-SA-4.0)

TL;DR:

  • Single exit principle is a relic of ancient technology
  • Always return early

Overview

As professional developers we read a lot of code, so it’s important to make sure other developers (and our future selves) have an easy time understanding what the code does. One common pattern I find hard to read (and easy to fix, btw) is deeply-nested if’s while trying to avoid an early return from a function. There are times when I’ve seen if’s nested about 8 deep, but I’m not exactly sure because my eyes glazed over and I got lost.

Talking to other developers about that pattern revealed a general preference for early returns, but a lot of people vaguely recall some college professor’s warning not to return early from a function, but they can’t remember why. So I figured it’s about time to write a blog entry to fully explain why it’s so much better to return early, and why your college professor was wrong when it comes to modern software best practices.

A Recent Example

The other day I was writing some UI code which had a number of preconditions to check, so I wondered what it would look like if I didn’t use early returns and just nested my ifs:

Without Early Returns (Bad)

function selectRangeRows(api: GridApi) {
const cellRanges = api.getCellRanges()
if (cellRanges && cellRanges.length > 0) {
//or else what?? the suspense is killing me!!
//(spoiler alert: nothing)
let startRow = cellRanges[0].startRow?.rowIndex
let endRow = cellRanges[0].endRow?.rowIndex
if (startRow != undefined && endRow != undefined) {
if (startRow != endRow) {
//swap the order so we can iterate
if (startRow > endRow) {
const tmp = startRow
startRow = endRow
endRow = tmp
}
//set rows selected
for (let i = startRow; i <= endRow; i++) {
api.getDisplayedRowAtIndex(i)?.setSelected(true, false, true)
}
}
//else, it's a single row, so let normal click handling do the selection. nothing to do
} //undefined start/endrow, nothing to do
} //no cell ranges. nothing to do, we're done here

With Early Returns (Good)

function selectRangeRows(api: GridApi) {
const cellRanges = api.getCellRanges()
if (!cellRanges || cellRanges.length < 1)
//no cell ranges. nothing to do, we're done here
return
let startRow = cellRanges[0].startRow?.rowIndex
let endRow = cellRanges[0].endRow?.rowIndex
if (startRow == undefined || endRow == undefined)
//undefined start/endrow, nothing to do
return
if (startRow == endRow)
//it's a single row, normal click handling do the selection. nothing to do
return

// failure cases / preconditions are done. Now do the stuff.
//swap the order so we can iterate
if (startRow > endRow) {
const tmp = startRow
startRow = endRow
endRow = tmp
}
//set rows selected
for (let i = startRow; i <= endRow; i++) {
api.getDisplayedRowAtIndex(i)?.setSelected(true, false, true)
}
}

This isn’t super complex, but the second way is much easier to follow. But why?

For one, there’s just fewer indentations, but also fewer scope blocks to keep track of in my head.

But the main reason for me is that it’s clearly defining the failure conditions and getting the heck out of there. It’s much easier to reason about all the possible failure conditions than it is to explicitly specify the happy path. It’s also much easier to add a new failure condition without affecting the other code or changing indentation — you just add it on the next line.

The General Antipattern

But in a more complex example, the dogma of “don’t return early” starts to get real nasty.

Here we have some standard example code with a little input checking and some function calls:

/**
* validate params, get supplemental data, and return final result
*/
String getStuff(SomeObject param1, SomeObject param2) {
String finalOutput = null;
if (param1.param1 != null) {
if (param1.isValid() {
if (param2 != null) {
if (param2.isValid() {
OtherObject output1 = getStuff(param1);
if (output1 != null) {
OtherObject output2 = getStuff(param2);
if (output2 != null) {
String finalOutput = getResult(output1, output2);
if (finalOutput == null) {
logErr("failed to get final output");
}
} else {
logErr("failed to get output from param2");
}
} else {
logErr("failed to get output from param1");
}
} else {
logErr("param2 was invalid");
}
} else {
logErr("param2 was null");
}
} else {
logErr("param1 was invalid");
}
} else {
logErr("param1 was null");
}
return finalOutput;
}

This visual pattern is known as “Arrow Code” — Flattening Arrow Code (codinghorror.com)

The error messages for the failure cases are so far from where they are checked! Good luck adding new failure case checks or complex business logic and getting those parens aligned correctly!

With Early Returns (Better)

Here’s what that same example looks like with early returns:

/**
* validate params, get supplemental data, and return final result
*/
String getStuff(SomeObject param1, SomeObject param2) {
if (param1.param1 == null) {
logErr("param1 was null");
return null;
}
if (!param1.isValid() {
logErr("param1 was invalid");
return null;
}
if (param1.param2 == null) {
logErr("param2 was null");
return null;
}
if (!param2.isValid() {
logErr("param2 was invalid");
return null;
}
OtherObject output1 = getStuff(param1);
if (output1 == null) {
logErr("failed to get output from param1");
return null;
}
OtherObject output2 = getStuff(param2);
if (output2 == null) {
logErr("failed to get output from param2");
return null;
}
String finalOutput = getResult(output1, output2);
if (finalOutput == null) {
logErr("failed to get final output");
return null;
}
return finalOutput;
}

Much easier to understand, right? Each error message is right next to the thing that it checks, and as requirements change I can add more functions and checks no problem. I feel pretty comfortable modifying this code.

“But wait, Phil, my professor back in college said to never return early! Dijkstra’s structured programming and single-exit principle and all that!”

Yeah, Dijkstra was a total genius, but back in his day they were working with flint and steel. And your professor was probably old and liked it that way.

Well guess what, I’m old too! Old enough to have written C code in a BSD kernel for a living, so I actually know why single-exit was important with ancient technology.

The reason for the single-exit principle is because of resource allocation and release, and if you forget to release a resource during a failure condition you’re in real big trouble.

With Early Returns (Bad, but Only in C and Similar)

Let’s check out what programming was like back in the day, and why returning early could lead to problems.

For you whippersnappers out there, a “handle” is just a reference to some system resource, like a file, socket, process, whatever. If you acquire a resource, it must be released or else you’ll get a leak and eventually run out of resources and crash the OS.

So let’s write a little kernel-style C code, but we’ll use early returns. Can you find the bug?

void* doStuffInC(void* input) {
int handle1 = acquireResource1(input.arg1);
if (handle1 < 0) { //failed to get resource 1
errno = -1;
return null;
}
int handle2 = acquireResource2(input.arg2);
if (handle2 < 0) {
errno = -2; //failed to get resource 2 - clean up
freeResource(handle1);
return null;
}
int handle3 = acquireResource3(input.arg3);
if (handle3 < 0) {
errno = -3; //failed to get resource 3 - clean up
freeResource(handle1);
return null;
}
void *output= getOutput(input.arg4, handle1, handle2, handle3);
freeResource(handle1);
freeResource(handle2);
freeResource(handle3);
return output;
}

That’s right, we totally forgot to free handle2 in that last error handler block!

Facepalm in front of computer
Fig 1: Whoever finds this bug three years later after spending two months debugging why systems are crashing in the field (“30/07/09 — Facepalm” by motti82 is licensed under CC BY-NC-ND 2.0.)

You see how easy it is to screw up in this simple example, imagine what it’s like with real code!

Without Early Returns (Good, but Only in C and Similar)

The way the ancients solved this problem was quite simple: goto!

void* doStuffInTheKernel(void* input) {
void* output = NULL;
int handle1 = acquireResource1(input.arg1);
if (handle1 < 0) {
errno = -1;
goto out;
}
int handle2 = acquireResource2(input.arg2);
if (handle2 < 0) {
errno = -2;
goto release1;
}
int handle3 = acquireResource3(input.arg3);
if (handle3 < 0) {
errno = -3;
goto release2;
}
output = getOutput(input.arg4, handle1, handle2, handle3);

freeResource(handle3);
release2:
freeResource(handle2);
release1:
freeResource(handle1);
out:
return output;
}

Yes, for real. It’s the ONLY acceptable reason to use goto. Ever. And it’s legit best practice, even now.

Don’t believe me? Let’s see what the Linux kernel docs have to say about it: https://www.kernel.org/doc/html/latest/process/coding-style.html?msclkid=9d898e35cd7c11ecb4fe5ca201b9d108#centralized-exiting-of-functions

Now that’s a lot easier to work with and check for correctness, not to mention modify if another resource needs to be used or more logic needs to be added.

Also notice how we don’t keep nesting those ifs! The goto flattens them by jumping past. What strange irony it is that goto results in cleaner code!

So yeah, in this type of programming language, it is important to avoid early returns.

Evolution of Technology: How We Got to “Early Returns are a Good Thing (tm)”

As we can see in the example, both Dijkstra and your old professor were right: avoiding resource leaks during failure conditions forced us to prefer a single exit point — but only with older technology. In other words, writing clean code and following established patterns helped us avoid major mistakes.

But with new tools came cleaner patterns, making resource management much safer to work with.

New Technology: Cleaner, Safer Code via RAII

In C++, the concept of RAII was established: Resource Allocation Is Instantiation.

What it means is when you acquire a resource, you actually create an object to wrap it — acquire the handle in the constructor, and release it in the destructor:

class Resource1() {
private int handle;
public Resource1(void* arg) {
handle = acquireResource1(arg);
if (handle < 0) throw Exception("Failed to acquire resource");
}
public getHandle() {
return handle();
}
public ~Resource1() {
freeResource(handle);
}
}

Then, whenever the object is freed, the resource is too. This means that if an exception is thrown anywhere down the stack, as your stack unwinds each frame automatically frees any object constructed in its frame. This means you don’t have to worry about explicitly releasing resources. It just works.

Now you can write code like the civilized, modern human that you are:

void* doStuffInCpp(void* input) {
Resource1 r1(input.arg1);
//if an exception is thrown here, r1 is freed
Resource2 r2(input.arg2);
//if an exception is thrown here, r1 and r2 are freed
Resource3 r3(input.arg3);
// if an exception is thrown here, or not, everything is freed
return getOutput(input.arg4, r1.getHandle(), r2.getHandle(),
r3.getHandle();
}

Now that’s clean code!

If you want to go real deep and nerd out, watch this — John Kalb, Exception-safe code: CppCon 2014: Jon Kalb “Exception-Safe Code, Part I” — YouTube

Ironically, this is one pattern that C++ gets cleaner than most other languages. Pretty much everything else in C++ though — yuck.

The Modern World: Dispose Pattern (aka AutoCloseable)

In modern garbage-collected languages like Java or C#, you still have to manage your resources properly via RAII, but the syntax is a bit different — and in true Java style, more verbose.

Note that we have to actually close the resources immediately after we’re done, we can’t simply wait for the GC to eventually get around to it — these are underlying system resources we’re working with, not simply bits of Java memory.

class Resource1 implements AutoCloseable {
private int handle = -1;

public Resource1(String arg) {
int handle = acquireResource(arg);
if (handle < 0)
throw new IllegalStateException("cannot acquire Resource1")
}
public int getHandle() {
return handle;
}

//called automatically when leaving a try-with-resources block
@Override
public void close() {
if (handle < 0) return;
freeResource(handle);
handle = -1;
}
}

So with that, you can wrap construction in a try block and everything gets cleaned up when the block exits for any reason.

SomeObject doStuffInJava(InputObject input) {
try(Resource1 r1 = new Resource1(input.arg1),
//if an exception is thrown here (like if arg2 is null,
//or any other reason specific to Resource2), r1 is freed
Resource2 r2 = new Resource2(input.arg2),
//if an exception is thrown here, r1 and r2 are freed
Resource3 r3 = new Resource3(input.arg3)) {
// if an exception is thrown here, or not, everything is freed
return getOutput(input.arg4, r1.getHandle(), r2.getHandle(),
r3.getHandle())
}
}

As always, Java is a little on the verbose side by requiring the try block, but at least it’s clear what’s going on, and gives you plenty of flexibility to handle your error cases.

Side note: It used to be recommended to implement a finalizer in your AutoCloseable class, to call close from when the Garbage Collector cleans up the object, just in case you forgot to use it in a try-with-resources block. However, there are a few problems with that approach:

  • You could still run out of resources before the GC gets around to cleaning up your mess.
  • Modern static analysis tools can easily detect when you use an AutoCloseable class outside of a try-with-resources block and flag it as a critical violation at build time.
  • Finalizers are deprecated anyway, for these reasons and many more: JEP 421: Deprecate Finalization for Removal (openjdk.org)

Conclusion

  • Modern tech obviates single-exit principle
  • Check your failure conditions, and return early
  • Write clean code

Thanks for bearing with me on this soapbox of mine. I hope this puts into context why returning early is better, and why that thing your professor said that one time about not returning early doesn’t apply to modern languages and patterns.

Looking forward to those nice clean code reviews!

About the Author

Phil Norton is a Senior Software Developer at Clearwater Analytics with a background in embedded systems, middleware, web development, management, and whatever else needs to be done at the time.

--

--