DEV Community

Cover image for Early Return in Elixir
Damon Janis
Damon Janis

Posted on

Early Return in Elixir

The only thing I've missed while working in Elixir the last few years is an early return.

In Javascript, you can do this:

let contrivedFunction = arg => {
  if(!arg) {
    return 'falsy';
  }

  if([] !== []) {
    return 'wtf';
  };

  // This will never be evaluated because JS is
  // a sad language where [] does not equal []
  return 'truthy';
};
Enter fullscreen mode Exit fullscreen mode

At any point, you can stop the execution of the current function and return a value, and nothing after that point will run.

Usually, in Elixir, you can get by just fine without that ability. You can use the with statement, pattern matching, or control flow like case and cond.

But sometimes, I really miss having an early return. Usually when I'm working in a really messy domain with a lethal combo of tech debt, third-party APIs, and a long chain of processing where things can go wrong in a lot of places.

I recently ran into a situation like that, and decided to do some digging. I knew that a function stopped execution as soon as an error was raised, so I thought I could maybe write a macro for that functionality. But then in the Elixir guides for try, catch, and rescue, I realized that the solution was hiding in plain sight:

def contrived_function(arg) do
  if !arg do
    throw("falsy")
  end

  if [] != [] do
    throw("wtf")
  end

  # Elixir is sane and [] does equal [], so this is possible
  "truthy"
catch
  value -> value
end
Enter fullscreen mode Exit fullscreen mode

It turns out, when you throw within a function, execution stops and you can catch (and return) the value! No macros or extra abstractions needed.

I don't anticipate that I'll use this pattern very much - in most cases the standard methods of control flow are easier to reason about and follow.

But, I hope that like me you appreciate having an extra tool in hand for those situations when it feels like the best way to express a bit of logic.

Discussion (5)

Collapse
rafaltrojanowski profile image
Rafał Trojanowski

I usually use pattern matching as a guard / return statement:

def contrived_function([]), do: []
def contrived_function(nil), do: []

or

def contrived_function(args) when is_list(args), do: (...)
def contrived_function(_), do: []
Enter fullscreen mode Exit fullscreen mode
Collapse
bigardone profile image
Ricardo García Vega

I use this approach as well :)

Collapse
hlship profile image
Howard M. Lewis Ship

I subscribe to the common belief that exceptions should be reserved for exceptional situations: actual failures.

You should most certainly profile this code vs. a more traditional approach. I would be surprised if you didn't see a huge performance gap ... throwing an exception is most languages is very costly, and disruptive to the kinds of optimizations that a compiler might make.

Collapse
damonvjanis profile image
Damon Janis Author • Edited on

I also subscribe to that belief, and it's how I've operated so far in my career in Elixir.

The interesting thing to me about throw/catch in strictly the Elixir context is that I've never actually seen it used for exceptions (or really anything else honestly). Typically the only thing you do with exceptions is rescue them, which isn't common but does come up occasionally. The one example I can think of is that the Bamboo email library until recent versions would raise instead of returning an :error tuple if the third-party email provider like SendGrid didn't send the email successfully, so you were forced to rescue if you wanted to handle an email failure gracefully.

I created a quick Benchee script on your recommendation, and these were the results on my 2013 MBP:

Comparison: 
return        6.31 M
throw         3.25 M - 1.94x slower +148.97 ns
**All measurements for memory usage were the same**
Enter fullscreen mode Exit fullscreen mode

Using a throw does interfere with tail call optimization, which is one of the tradeoffs you mentioned might be present. It looks like the code with the throw is about half as fast as the one with the return, so there is somewhat of a performance penalty.

I really am not recommending that anyone takes this as a pattern and uses it extensively instead of the usual control flow structures in Elixir. It's just an interesting quirk of the language that a rarely-used BIF plus the elegant syntax for function level catch / rescue handling turns out to implement an early return with surprisingly little ceremony.

EDIT: the original results I posted showed that the throw version was faster, but I realized there was a bug in my script and I was calling the throw version in both cases. I updated it, and the throw version is about twice as slow.

Collapse
mrmicahcooper profile image
Micah Cooper

I usually reach for a cond if I want an early return-like function.