Exit Your Functions Early

Aug 17, 2024

Early on in my programming career, I was given the advice that a function should only have a single return statement. At the time I took the advice to heart, but I now consider it one of the worst pieces of programming advice I have ever been given.

Here's a simple example of only having a single return statement:

async function inviteUserIfNecessary(name: string, email: string): Promise<User> {
  let user = await getUserByEmail(email);
  if (!user) {
    user = await createUser(name, email);
    await sendUserWelcomeEmail(user);
  }

  return user;
}

Although this simple function isn't too bad with only a single return statement, it can be cleaned up.

Reduce Nesting

In the above function, we are creating the new user and sending a welcome email inside of an if statement, which means our function contains nested code. Nested code is inevitable and is not something to avoid at all costs, but it is worth avoiding if it's easy enough to do so.

Let's look at what happens if we allow ourselves to have multiple return statements within the function:

async function inviteUserIfNecessary(name: string, email: string): Promise<User> {
  const existingUser = await getUserByEmail(email);
  if (existingUser) return existingUser;

  const newUser = await createUser(name, email);
  await sendUserWelcomeEmail(newUser);

  return newUser;
}

We've managed to get rid of the nesting by returning early when we have an existing user. In such a simple function like this, that's a small win, but in a more complex function that already has a lot of nesting, removing unnecessary nesting can make a big difference in the readability of the function.

Better Variable Names, Fewer Bugs

If you take another look at the two different implementations, you'll also notice that by returning early, I was able to use more descriptive variable names. Rather than having to share a single user variable that can either be the existing user or a new user, I was able to declare separate variables.

Why is that better? Imagine we have the following bug in our code:

async function inviteUserIfNecessary(name: string, email: string): Promise<User> {
  let user = await getUserByEmail(email);
  if (!user) {
    user = await createUser(name, email);
  }

  // This is the bug, we're sending a welcome email to an existing user.
  await sendUserWelcomeEmail(user);
  return user;
}

Sending the welcome email to an existing user is obviously a mistake, but if we look at the code itself, the bug is not immediately obvious. sendUserWelcomeEmail(user) doesn't stand out as obviously incorrect. In fact, it's the exact same line of code as the correct implementation, except we didn't put it within the if statement.

Now let's look at what it looks like to introduce this same bug in our function that returns early:

async function inviteUserIfNecessary(name: string, email: string): Promise<User> {
  const existingUser = await getUserByEmail(email);
  if (existingUser) {
    await sendUserWelcomeEmail(existingUser);
    return existingUser;
  }

  const newUser = await createUser(name, email);
  await sendUserWelcomeEmail(newUser);

  return newUser;
}

Take a look at the bug in that version of the function:

await sendUserWelcomeEmail(existingUser);

There's an obvious verbiage mismatch here - we're sending a welcome email but we're passing in a variable named existingUser. Alarm bells are much more likely to go off in our head when we type this line out. By having a separate variable for newUser and existingUser, we are more likely to avoid this bug altogether.

How to spot this pattern

When I'm reviewing someone's code, I try to be on the lookout for this general pattern:

function doSomething() {
  if (xxxx) {
    xxxxx
    xxxxx
    xxxxx
    xxxxx
  }
}

A function that contains this shape can very likely be refactored to remove the nesting:

function doSomething() {
  if (!xxxx) return;

  xxxxx
  xxxxx
  xxxxx
  xxxxx
}

This is a very simple way to make your code easier to read and understand, which ultimately leads to fewer bugs.