đŞ There Is No Return

What a language learns when it loses the escape hatch.
Steven B Miers ¡ November 2026 ¡ v1.06
đ Introduction
Doors and corners, kid. Thatâs where they get you. â Detective Miller, in James S. A. Coreyâs Leviathan Wakes (2011)
Elixir has no return keyword. Not deprecated, not frowned on in a style guide â absent. Type one anyway and the compiler, having no idea what you mean, goes looking for a function named return and reports, a little sadly, that there isnât one. Erlang never had one either. Rust, Scala and Ruby technically do, and the advice in each is some flavor of âdonâtâ â many Ruby shops, including all of the ones the author has led, ban it in the style guide, so Ruby keeps return mainly to give the linter a hobby. Java, meanwhile, spent 2017 to 2020 and three JEPs teaching switch to hand back a value at all [1].
In an expression-oriented language, the value is the exit. There is no door marked return because every hallway already ends in a room.
This is not a functional-programming sermon. ALGOL 68 was expression-oriented in 1968 and thoroughly imperative [2]. The claim is narrower and stranger: when every construct must produce a value, the early return doesnât die. It gets absorbed â and comes back with a type signature.
đ§ą Everything is a value, including nothing
Concepts: conditional expressions ¡ block expressions ¡ expression-bodied functions ¡ case (match) expressions ¡ the unit value
Elixirâs Getting Started guide states the rule in one breath: âIn Elixir, there are only expressions, no statements. Everything you write in Elixir language returns some valueâ [3].

Fig. 1: Statements act, expressions answer. A statement does something and hands back nothing; void isnât a value, so it canât be passed or assigned. An expression always hands back a value â and in an expression-oriented language, every construct has the outlet.
A conditional expression has a value, even when it has nothing to say:
# Elixir
iex> if 7 > 5, do: "yes"
"yes"
iex> if 3 > 5, do: "yes"
nil
That nil isnât Elixir being polite. The if macro quietly bolts on an else: nil and expands to a case, and a case is not allowed to shrug [4].
A block expression is worth its last line. Rust spells it out [5]:
// Rust
let x = { (); 0u8 }; // 0
let y = { 0u8; }; // () â the semicolon threw the value away
Ruby, Elixir and Scala agree, which is why none of them need return to say what a function produces.
An expression-bodied function is a function that is nothing but that value. In Rust the body is a block, so the tail expression is the return [5]; C# calls the same thing an expression-bodied member; Elixir does it with a keyword list. This can be accomplished in Python via a decorator (more below):
// Rust
fn two() -> u8 { 2 } // the block's last expression is the value
# Elixir
def two, do: 2
# Python 3.11+ ¡ modgud 2.0
from modgud import implicit_return
@implicit_return()
def two():
2
And a case expression is worth whatever the winning arm is worth:
# Elixir
label = case fetch(id) do
{:ok, user} -> user.name
{:error, reason} -> "unknown (#{reason})"
end
If no arm wins, Elixir raises CaseClauseError rather than quietly handing back nil [4].
đ§ą Guard clauses: the orthodoxy with no receipts
Concepts: guard clauses ¡ early return ¡ the nesting-depth rule
Here is the obvious objection. A language with no return canât do this:
# Python
def ship(order):
if order is None: return None
if not order.items: return None
if not order.paid: return None
# the actual work, flat and unindented
Guard clauses. Early returns. Martin Fowlerâs catalog calls the move âReplace Nested Conditional with Guard Clauses,â and the entry lists the prerequisites, the mechanics, and not a single drawback [6] â the one refactoring in the catalog with no downside, which ought to make a person suspicious. The author has shipped that return None on line three more times than he would admit under oath.
So where are the receipts? Thin. The famous rule â never nest past three levels, few humans can follow more â traces to a software-quality indicator page that cites style guides from 1976 to 1987 and no studies at all [7]. The nesting objection to expression-oriented code and the case for guard clauses stand on the same unmeasured ground. Neither side gets to bring data to this fight, so both sides bring volume.

Fig. 2: The pyramid, the guard, the head. Guard clauses flatten the pyramid by adding doors. The third panel flattens it by removing the body the doors were in â thatâs the next section.
đ Python with a twist: a bonus fourth way. Python canât put the guards in the function head, but a decorator can put them just above it. modgud runs each guard before the body â a guard is any callable that answers True or a message â and makes the last expression of each branch the functionâs value, so the body has no return at all. The same ship, doors up top, one value out the back:
# Python 3.11+ ¡ modgud 2.0
from modgud import Guards, guarded_expression
@guarded_expression(
Guards.not_none("order"),
lambda order, *_, **__: bool(order.items) or "no items",
lambda order, *_, **__: order.paid or "unpaid",
)
def ship(order):
if order.express:
courier(order)
else:
post(order)
A failed guard raises GuardClauseError with the message; the body never starts. More on how it does this, and what it borrows from Elixir, in What replaced the door.
đŁ The 174 â 33 problem
Concepts: non-local return ¡ return inside a block expression
What there is evidence for is what an early return does inside a language where blocks are values. Rob Norrisâs Scala demonstration: take a helper that returns early, inline it into a foldâs lambda, and the sum changes from 174 to 33 â because the keyword exits the enclosing method, not the lambda, and does it by throwing an exception that carries no stack trace [8]. An early return inside a block that is a value is a time bomb with the countdown display unplugged â it still goes off; you just donât get to watch. Scala 2.13 added a lint for it. Elixir skipped the lint by skipping the keyword.
đ¤ď¸ What replaced the door
Concepts: guards in function heads ¡ multi-clause functions ¡ railway chaining (with) ¡ the ? operator ¡ let-else ¡ decorator guards with implicit return
Nobody deleted early return. They moved it into the grammar, and every language picked a different spot.
âComposing simple components, simple in that same respect, is the way we write robust software.â â Rich Hickey, Simple Made Easy (2011)
Guards in the function head. Elixir puts the condition on the clause, not in the body:
# Elixir
def area({:circle, r}) when r > 0, do: 3.14159 * r * r
def area({:rect, w, h}) when w > 0 and h > 0, do: w * h
def area(_), do: {:error, :bad_shape}
Three guard clauses, written as pattern matching, and nothing to flatten because there is no body to flatten it out of. The docs say it outright: developers âprefer to use pattern matching and guards in function definitionsâ over nested conditionals [3].
The railway. For a chain of steps that can each fail, Elixirâs with is a sequence of guarded early returns expressed as one value. Every <- either matches and continues, or the chain stops and hands back the thing that didnât match [4].
# Elixir
with {:ok, user} <- fetch(id),
{:ok, order} <- validate(user, params),
{:ok, receipt} <- charge(order) do
{:ok, receipt}
end

Fig. 3: The with railway. Three stations on the main line, each with a siding to the else platform, and every track ends in a value. There is no door.
The typed escape hatch. Rust kept return and then built two better ones. The ? operator is an early return with a type-directed trigger â it leaves the function only when the Result says so. And let ⌠else, stable since Rust 1.65, is a guard clause with pattern matching. The RFC that introduced it concedes, in its own motivation, that if let âcan force rightward drift,â and cites a refactor âshowing the value of early-returnsâ [9]. The people who built the expression language bolted a statement-shaped hatch back onto it â on purpose, in writing.
The decorator. Python is a statement language and intends to stay one. PEP 635 rejected an expression form of match because the result would be âunusually long and complexâ [10] â and the Zen of Python was there first: âReadability countsâ [11].
The author, lacking the standing to argue with Guido, wrote a package instead, which is how engineers converse. modgud wraps a function in guard clauses that run first â a failed guard raises GuardClauseError and the body never starts â plus an implicit return that makes the last expression of each branch the value [12]. A guard is any callable that answers True or a message. Here is the ship function from the pyramid, with its three doors moved above the body:
# Python 3.11+ ¡ modgud 2.0
from modgud import Guards, guarded_expression
def has_items(order, *_, **__):
return bool(order.items) or "no items"
def is_paid(order, *_, **__):
return order.paid or "unpaid"
# @implicit_return is implicit with use of @guarded_expression, and can be disabled
@guarded_expression(Guards.not_none("order"), has_items, is_paid)
def ship(order):
if order.express:
courier(order)
else:
post(order)
Version 2.0 goes one step further and lends Python a conditional expression with Elixirâs vocabulary:
# Python 3.11+ ¡ modgud 2.0
from modgud import eop_clause, eop_cond, implicit_return
@implicit_return()
def sign(x):
eop_cond(eop_clause(x < 0, "negative"), eop_clause(x == 0, "zero"), default="positive")
Itâs named for Móðguðr, who guards the bridge to Hel in the Norse stories. The resemblance to âmod-goodâ is not an accident, and neither is the fact that it works by rewriting the AST, which is the polite term for a trick. Python will keep being Python. But the shape â guards up front, one value out the back â is the shape Elixir enforces and Rust types. ~Móðguðr â
â ď¸ Beware!
Concepts: flattened error handling ¡ error normalization ¡ function granularity
The best critique comes from the most honest place: Elixirâs own documentation, under a heading literally titled Beware! Every failure in a with lands in the same else block, so âitâs difficult to know from which clause the error value came,â and the official anti-patterns guide names âcomplex else clauses in withâ as a smell [4][13]. The fix the docs offer â normalize each stepâs errors in its own private function â points where every remedy in this style points: more, smaller functions. People who live in it report that that is the real cost. Not nesting. Navigation.
đ§ Monday morning, in any language
Concepts: switch expressions ¡ the late-initialized temporary ¡ exhaustiveness
Two linters in two ecosystems enforce the same rule. Googleâs Error Prone flags a variable declared, then assigned in every arm of a switch statement, and offers to fold the three into one [14]:
// Java 14+ (switch expressions, JEP 361)
int score; // before
switch (suit) {
case HEARTS: case DIAMONDS: score = -1; break;
case SPADES: score = 2; break;
case CLUBS: score = 3; break;
}
int score = switch (suit) { // after
case HEARTS, DIAMONDS -> -1;
case SPADES -> 2;
case CLUBS -> 3;
};
Rustâs Clippy flags the same shape in a match, under the name needless_late_init [15]. The tell is a mutable temporary that every branch assigns â and the expression form is the one the compiler checks for a missing arm. You can run that check by eye, in Java, before going anywhere near a language that has no return at all.
đď¸ The door was never the point
Expression-oriented languages didnât win the argument against early return. They absorbed it.

Fig. 4: The escape-hatch ladder. Every rung is an early return. What changes is how loudly the function says so â and whether the compiler gets to check the exit.
The question left in 2026 isnât expressions or statements. Itâs which escape hatches, and how loudly a function has to announce that itâs leaving.
Underneath every one of those hatches sit the same four preferences:
- Expressions over statements. Computations produce values rather than merely perform actions.
- Composition over sequence. Complex behavior emerges from combining smaller expressions.
- Abstraction over ceremony. Code should communicate meaning without unnecessary machinery.
- Clarity over mechanics. A program should describe a computation, not merely enumerate instructions.
Try this Monday: find one function with a mutable result variable assigned in every branch. Make it an expression. If the compiler finds a branch you didnât â that was the bug.
The author has never designed a language, only lived in several. The research behind this piece, every quote with its source, runs to fifteen thousand words; this is the short version.
đ References
- JEP 361: Switch Expressions. OpenJDK, final in Java 14 (2020). https://openjdk.org/jeps/361
- van Wijngaarden, A., et al. Report on the Algorithmic Language ALGOL 68, §0.1.2 âOrthogonal design,â 1968.
- Elixir Getting Started, âcase, cond, and if.â https://elixir.hexdocs.pm/case-cond-and-if.html
- Elixir v1.20.0,
KernelandKernel.SpecialFormsdocumentation (if/2,case/2,with/1,defmodule/2). - The Rust Reference, âStatementsâ and âBlock expressions.â
- Fowler, M. âReplace Nested Conditional with Guard Clauses.â Refactoring, 2nd ed., 2018.
- IFSQ indicator SP-2, âNesting.â https://www.ifsq.org/indicator-SP-2.html
- Norris, R. âDonât Use Return in Scala,â 9 May 2014. https://tpolecat.github.io/2014/05/09/return.html
- Rust RFC 3137,
let-elsestatements, 2021. https://rust-lang.github.io/rfcs/3137-let-else.html - PEP 635, Structural Pattern Matching: Motivation and Rationale, âStatement vs. Expression.â https://peps.python.org/pep-0635/
- Peters, T. âThe Zen of Python,â PEP 20, 2004. https://peps.python.org/pep-0020/
modgud2.0.0 on PyPI, November 2026. https://pypi.org/project/modgud/- Elixir, âCode-related anti-patterns.â https://elixir.hexdocs.pm/code-anti-patterns.html
- Error Prone,
StatementSwitchToExpressionSwitch. https://errorprone.info/bugpattern/StatementSwitchToExpressionSwitch - Clippy,
needless_late_init.
Version 1.06 ¡ November 2026