eph baum dot dev

← Back to blog

From Early Return in OOP to Control Flow in Elixir - A Transition Guide

Published on 10/17/2023 05:48 PM by Eph Baum

Featured Image

Introduction

When transitioning from an Object-Oriented Programming (OOP) language like Java or Python to a Functional Programming (FP) language like Elixir, one of the first differences you may notice is the absence of a return keyword.

If you come from a PHP background, particularly with Laravel experience, transitioning to Elixir can be a paradigm shift. The “early return,” a practice commonly used to handle exceptional or negative conditions without traversing through the entirety of a function. In this blog post, we’ll discuss what early return is and how similar outcomes can be achieved in Elixir.

This came up recently in a conversation and I thought it might make a good blog post, so here we go:

The Value of Early Return

In PHP, and particularly in Laravel, early return is often employed to exit a function as soon as an exceptional or negative condition is met. This not only improves code readability but can also offer performance optimizations.

Consider the following PHP example:

function validateInput(string $input): string
{
    if (empty($input)) {
      return "Input is empty";
    }
    
    // Additional validation logic
    return "Input is valid";
} 

A contrived and simplistic example of the “Return Early Pattern”

In this example, the function returns immediately if the input is empty, preventing any further validation logic from executing. The alternative of returning later, after perhaps operating on the value, passing it through additional methods

Obviously this is a fairly simplistic and contrived example, but it illustrates how such a pattern doesn’t quite make sense in the FP world of Eilxir.

The Elixir Way: No return, No Problem

In Elixir, functions don’t have return statements, but there are alternative paradigms to achieve similar outcomes.

1. Pattern Matching

Pattern matching can serve as a declarative way to specify different function behaviors based on input.

def find_element([], _target), do: false 
def find_element([head | tail], target) when head == target, do: true 
def find_element([_ | tail], target), do: find_element(tail, target) 

Essentially overloading functions allows you to run only the logic that matches the pattern of the inputs.

Elixir trends toward conciseness in its functions, even when you’re defining the same function for different cases.

To explain: the first clause is an “empty list” and matches when the list is empty. In this case _target is ignored since we must return false as there’s no element to find. The second clause is “head matches target” when the first item of the list is a match to target the function simply stops recursing and returns true to the guard clause. The third clause is a “catch-all”: If the head of the list doesn’t match the target, this clause catches it. The _ ignores the head of the list as it’s not needed. The function then recurs with the remaining tail of the list to continue the search.

So in practice, Elixir will execute the clause that first matches the pattern and satisfies any guard clauses. This eliminates the need for an explicit return statement by leveraging pattern matching and recursion for flow control.

2. Guard Clauses

Guard clauses enforce conditions right in the function signature, making the code clean and readable.

def is_negative?(x) when x < 0, do: true
def is_negative?(_), do: false

Here we check the value of x and if it’s less than 0 you’ll return true while the _ “wildcard” placeholder will match anything not matched in the first clause and return false.

3. case or cond Statements

When pattern matching and guard clauses are not sufficient, case or cond statements can offer more flexibility.

def evaluate(x) do
  case x do
    x when x < 0 -> :negative
    x when x > 0 -> :positive
    _ -> :zero
  end
end

def evaluate(x) do
  cond do
    x < 0 -> :negative
    x > 0 -> :positive
    true -> :zero
  end
end

These are, practically, the same constructs. Ultimately this code evaluates x and returns the appropriate atom result.

4. Throw and Catch

Though less idiomatic in Elixir, throw and catch can be used for early exits in certain situations. This should be used sparingly, or not-at-all.

def risky_operation(x) do
  try do
    if x == :dangerous_value do
      throw(:abort)
    end
    {:ok, x}
  catch
    :throw, :abort -> {:error, :aborted}
  end
end

An example of throw / catch in Elixir - probably don’t do this

The use of throw and catch in Elixir is generally considered non-idiomatic but can be useful for specific situations where you need an immediate, non-local exit from a deeply nested operation. It’s similar in some ways to early return when you need to abort execution based on a particular condition.

While throw and catch offer a way to implement early return-like behavior, they should be used cautiously, as they can make the code harder to follow. Their use is often discouraged in favor of more idiomatic Elixir constructs like pattern matching, guard clauses, case, or cond.

Conclusion

In summary, when transitioning from an object-oriented programming background with languages like PHP to a functional paradigm like Elixir, the concept of “early return” undergoes a shift. Rather than using explicit return statements, Elixir relies on pattern matching, guard clauses, and constructs like case and cond for flow control.

While this may initially seem unconventional, understanding and embracing these idiomatic Elixir practices can lead to more maintainable, readable, and idiomatic code. Additionally, these functional paradigms help to create code that’s easier to test, debug, and reason about, since functions become more predictable and side-effect-free. They also align better with Elixir’s core principles, which prioritize immutability and statelessness.

Although Elixir does offer constructs like throw and catch for non-local exits, these are generally reserved for exceptional cases and are considered non-idiomatic for typical control flow. By leaning into the native features of Elixir for managing control flow, developers can write more effective and idiomatic Elixir code.

Written by Eph Baum

  • Making Brutalist Design Accessible: A Journey in WCAG AA Compliance

    Making Brutalist Design Accessible: A Journey in WCAG AA Compliance

    How I transformed my brutalist blog theme to meet WCAG AA accessibility standards while preserving its vibrant, random aesthetic. Talking about contrast ratios, color theory, and inclusive design.

  • Building Horror Movie Season: A Journey in AI-Augmented Development

    Building Horror Movie Season: A Journey in AI-Augmented Development

    How I built a production web app primarily through 'vibe coding' with Claude, and what it taught me about the future of software development. A deep dive into AI-augmented development, the Horror Movie Season app, and reflections on the evolving role of engineers in the age of LLMs.

  • Chaos Engineering: Building Resiliency in Ourselves and Our Systems

    Chaos Engineering: Building Resiliency in Ourselves and Our Systems

    Chaos Engineering isn't just about breaking systems — it's about building resilient teams, processes, and cultures. Learn how deliberate practice strengthens both technical and human architecture, and discover "Eph's Law": If a single engineer can bring down production, the failure isn't theirs — it's the process.

  • Using LLMs to Audit and Clean Up Your Codebase: A Real-World Example

    Using LLMs to Audit and Clean Up Your Codebase: A Real-World Example

    How I used an LLM to systematically audit and remove 228 unused image files from my legacy dev blog repository, saving hours of manual work and demonstrating the practical value of AI-assisted development.

  • Migrating from Ghost CMS to Astro: A Complete Journey

    Migrating from Ghost CMS to Astro: A Complete Journey

    The complete 2-year journey of migrating from Ghost CMS to Astro—from initial script development in October 2023 to final completion in October 2025. Documents the blog's 11-year evolution, custom backup conversion script, image restoration process, and the intensive 4-day development sprint. Includes honest insights about how a few days of actual work got spread across two years due to life priorities.

  • 50 Stars - Puzzle Solver (of Little Renown)

    50 Stars - Puzzle Solver (of Little Renown)

    From coding puzzle dropout to 50-star champion—discover how AI became the ultimate coding partner for completing Advent of Code 2023. A celebration of persistence, imposter syndrome, and the surprising ways generative AI can help you level up your problem-solving game.