Jake Worth

One Way Out of a Function

Published: May 02, 2022 3 min read

A programming style that I like was described to me as: “one way out of a function.” The idea is that early returns cause confusion, and when possible, return once.

Imagine a flash message function that takes an HTTP response object responding to .ok, returning a string. Here’s the implementation with early return:

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

Some might omit the else block and just return the second string. Functionally these are the same.

And my preference, without 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 argument. And it mutates the message variable! We’re talking lesser of two evils here. And I think early returns are the greater evil. I offer two arguments: they build dead code into the function, and they’re harder to read.

Early returns build an opportunity for dead code into the function. Is the code below the first return ever evaluated? Hard to say! Let’s hope that code isn’t broken in some way. With one return, we can say that the whole function is exercised top-to-bottom.

I also find early returns, especially many, hard to read. With a single return we can say: “we create a variable, change it based on some conditions, and then return it.” With multiple returns, it’s something like “we 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 wins.

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 the language handle our conditional-style logic.

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, because there’s no additional code that will be evaluated:

function flashMessage(response) {
  if (response.ok) {
    // debugger here?
    return 'It worked'
  } else {
    // and/or here?
    return 'It failed'
  }
}

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

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

  // debugger here; 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'
  }
}

Counterargument: Guard Clauses

Guard clauses are one case where I think early returns are useful. They tell to me: “if this condition is true, ignore the rest of the function.”

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

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

  // rest of the function
}

I like guard clauses because they identify a unique state where the function doesn’t have work to do.

Your Turn

Do you use early returns? When, and why? Let me know by commenting on this post.

✉️ Get better at programming by learning with me. Subscribe to Jake Worth's Newsletter for bi-weekly ideas, creations, and curated resources from across the world of programming. Join me today!


Blog of Jake Worth, software engineer in Maine.

© 2022 Jake Worth.