Jake Worth

Jake Worth

One Way Out of a Function

Published: May 02, 2022 • Updated: March 13, 2023 3 min read

  • javascript

A programming style I practice could be described as: “there should be one way out of a function.” Early returns can cause confusion, and when possible, should be avoided in favor of a single return.

This blog post demonstrates this style.

Common Code: Early Returns

Imagine a message-producing function that takes an HTTP response object responding to .ok and returns a string. Here’s an implementation, with an early return when response.ok is truthy:

function flashMessage(response) {
  if (response.ok) {
    return 'It worked'
  } else {
    return 'It failed'
  }
}

Some might omit the else and just return the second string if response.ok is falsy.

function flashMessage(response) {
  if (response.ok) {
    return 'It worked'
  }

  return 'It failed'
}

Functionally these are the same.

My Preference: One Way Out

Here’s the same code without the early returns.

function flashMessage(response) {
  let message
  if (response.ok) {
    message = 'It worked'
  } else {
    message = 'It failed'
  }

  return message
}

I chose a trivial example so I’d have to make a good argument1. I offer three:

Early returns:

  • Build dead code into the function
  • Are harder to read
  • Are harder to debug with tooling

Dead Code Built In

Early returns build an opportunity for dead code into the function. Is response.ok ever falsy? It’s impossible to say with just this amount of context. Maybe response is an object like this:

const response = { ok: true }

Or maybe, our API is broken and it always returns ok. In either case, our else is dead code. This happens. We can’t really know without some digging. The opportunity for dead code is there.

Harder to Read

I find early returns, especially many of them, hard to read.

With a single return we can say: “we create a variable, adjust it based on some conditions, and then return it.”

With multiple returns, it’s more like “check something, then maybe return, or check something else, then maybe return, or…”, etc. If a measure of readability is how simply the code can be translated into words, one return is better.

Harder to Debug

Early returns makes debugging harder, too. If you want to know which conditional is evaluating with early returns, you have to stick a debugger into each one:

function flashMessage(response) {
  if (response.ok) {
    // Debugger here?
    return 'It worked'
  } else if(response.notFound) {
    // ...and here?
    return 'That record does not exist'
  } else {
    // ...and here? etc.
    return 'It failed'
  }
}

With a single return, you can use one debugger statement and know it will evaluate:

function flashMessage(response) {
  let message
  if (response.ok) {
    message = 'It worked'
  } else if(response.notFound) {
    message = 'That record does not exist'
  } else {
    message = 'It failed'
  }

  // A debugger here is always evaluated!
  return message
}

And it rarely stays this simple. You end up adding more conditionals, and the returns grow. When there are five conditionals, those early returns become tough to reason about.

function flashMessage(response) {
  if (response.ok) {
    return 'It worked'
  } else if (response.unauthorized) {
    return "You can't do that"
  } else if (response.notFound) {
    return 'That record does not exist'
  } else if (response.unprocessable) {
    return 'Could not update with the data you provided'
  } else {
    return 'It failed'
  }
}

This is a gnarly function know matter how you write it. At least with a single return, we know the way “in” and “out” of the function.

Counterargument: Guard Clauses

Guard clauses are an exceptional case where I think early returns work. They tell to me: “if this condition is true, exit!”

To revisit our example, perhaps we shouldn’t return a message if the response doesn’t respond to ok, because that indicates a bad response. Here’s a guard clause with optional chaining.

function flashMessage(response) {
  if (response?.ok === undefined) return

  // ...rest of the function
}

I like guard clauses because they identify a unique state where the function should bail.

Alternatives

My recent post Hash Fetch Instead of If/Else offers one example of an alternative to early returns. By using Ruby’s fetch, we let a language feature handle our conditional-style logic.

Do you use early returns? When, and why?


  1. And it mutates the message variable! We’re talking lesser of two evils here. I think early returns are the greater evil.

What are your thoughts on this? Let me know!


Get better at programming by learning with me! Join my 100+ subscribers receiving weekly ideas, creations, and curated resources from across the world of programming.