Implicit Return
In Ruby, a method always returns something, even if you never write the word return. Every method hands back the value of the last expression it evaluates. This is called implicit return, and it is one of the most Ruby-flavored features of the language: it grows directly out of the fact that in Ruby, almost everything — an if, a case, an assignment, a block — is itself an expression with a value. Understanding implicit return well means you stop being surprised by what your methods hand back, and you start writing shorter, more idiomatic Ruby.
Overview / How it works
Ruby is an expression-oriented language. Almost every construct you can write — an if/elsif/else, a case, a begin/rescue, even a bare assignment — evaluates to a value. A method body is simply a sequence of expressions, and when Ruby finishes executing a method, it does not need you to say what to hand back: it automatically returns the value produced by the last expression that was evaluated, in whatever branch execution actually took.
This is different from languages like Java or JavaScript (in most of its syntax), where a function returns undefined/void unless you write an explicit return statement. In Ruby, the explicit return keyword still exists and is fully supported — it is required whenever you want to exit a method early, before reaching its last line — but at the very end of a method it is redundant. Idiomatic Ruby style omits it there.
Because control-flow structures are expressions too, the “last expression” is not necessarily the last line of source code — it is the last expression evaluated along the path execution actually followed. If a method ends with an if/elsif/else, the method returns whatever that particular branch evaluated to. If it ends with a case, it returns whatever the matching when or in clause evaluated to. This lets you write methods that read like a description of a value rather than a sequence of commands.
Ruby 3.0 added a further, more compact way to lean on implicit return: the endless method definition, def square(x) = x * x. Because the body is a single expression, there is nothing to return except that expression’s value — implicit return is, in a sense, the entire point of the endless method syntax.
One consequence worth internalizing early: Ruby’s truthiness rules matter here too. Only nil and false are falsy; everything else — including 0, "", and [] — is truthy. If your method’s last expression is a conditional, the value it implicitly returns follows these same rules, and a branch that “does nothing” typically still returns nil (the value of an if with no matching branch and no else).
Syntax
def method_name(param1, param2)
expression1
expression2
last_expression
end
# Endless method (Ruby 3.0+): the single expression is the return value
def short_method(param1, param2) = expression
def ... end— the traditional method definition; the value oflast_expression, whichever one is actually reached, becomes the method’s return value.expression1,expression2— earlier statements in the body. Their values are computed but discarded; only the final one is kept.def method_name(...) = expression— the endless method form. The body is exactly one expression, and that expression’s value is always what gets returned.return(not shown above) — an explicit keyword you can place anywhere in a method body to exit immediately with a given value. It is optional on the final line but required for any early exit.
| Form | When to use it |
|---|---|
def name(args)…end |
Multi-line method bodies, or any method with conditionals, loops, or multiple statements |
def name(args) = expr |
A short, single-expression method — a predicate, a simple calculation, a one-line wrapper |
Examples
Example 1: a basic implicit return. The method’s only line is the value that comes back — no return keyword needed.
def square(x)
x * x
end
puts square(5)
Output:
25
x * x is the last (and only) expression evaluated inside square, so its value, 25, is what the method hands back to puts.
Example 2: the endless method form. Since the whole body is one expression, implicit return is baked into the syntax.
def cube(x) = x * x * x
puts cube(3)
Output:
27
cube is defined in one line. There is no body to “fall through” — the single expression x * x * x is evaluated and returned directly.
Example 3: implicit return through a conditional. This is the case that trips people up the most: the method’s last line is end, but the last expression evaluated is inside whichever branch actually ran.
def classify(n)
if n.negative?
"negative"
elsif n.zero?
"zero"
else
"positive"
end
end
puts classify(-5)
puts classify(0)
puts classify(10)
Output:
negative
zero
positive
For classify(-5), n.negative? is true, so the whole if expression evaluates to "negative", and since that if is the last (and only) expression in the method body, "negative" becomes classify‘s return value. The same reasoning applies to each call — Ruby returns whatever the taken branch produced, not “the last line of source code.”
How it works step by step
Walking through classify(0) the way the interpreter does:
- Ruby calls
classifywithnbound to0. - The method body contains one top-level expression: the
if/elsif/else. Ruby begins evaluating it. n.negative?is checked first:0.negative?isfalse, so that branch is skipped.n.zero?is checked next:0.zero?istrue, so Ruby evaluates that branch’s body, the string literal"zero", and theelsebranch is never reached.- The whole
ifexpression’s value is now"zero"— that is what anif/elsif/elseexpression evaluates to: the value of whichever branch actually ran. - Since that
ifwas the last expression evaluated inclassify‘s body, the method returns"zero"to its caller. - Back at the call site,
puts classify(0)receives"zero"and prints it followed by a newline.
Notice that no return keyword appeared anywhere. Ruby did not need one: it tracked the value of the expression it was evaluating and handed that value back the moment the method body finished.
Common Mistakes
1. Ending a method with puts instead of the value you meant to return
puts always returns nil. If it is the last expression in your method, your method returns nil — even though it printed the right thing.
def add(a, b)
result = a + b
puts result
end
x = add(2, 3)
puts x.inspect
Output:
5
nil
add prints 5 as a side effect, but its last expression is the call to puts, which itself evaluates to nil. So add implicitly returns nil, and x ends up nil instead of 5. The fix is to make the value, not a call to puts, the last expression:
def add(a, b)
a + b
end
x = add(2, 3)
puts x
Output:
5
2. Reaching for each when you meant map
Array#each always returns the original, unmodified array — whatever the block computes is thrown away. If a method’s last expression is an each call, that is what gets implicitly returned, which surprises people who wanted the transformed values.
def doubled(arr)
arr.each { |n| n * 2 }
end
result = doubled([1, 2, 3])
puts result.inspect
Output:
[1, 2, 3]
Each element was doubled inside the block, but those doubled values were never collected anywhere — each‘s return value is always the receiver itself. Use map, which builds and returns a new array of the block’s results:
def doubled(arr)
arr.map { |n| n * 2 }
end
result = doubled([1, 2, 3])
puts result.inspect
Output:
[2, 4, 6]
3. Trusting a bang method to always return the array
Several mutating (!) methods return nil specifically when they made no change, rather than returning self unconditionally. If one of these is your method’s last expression, an implicit nil can leak out even though the input array is perfectly fine.
def remove_duplicates(arr)
arr.uniq!
end
result = remove_duplicates([1, 2, 3])
puts result.inspect
Output:
nil
[1, 2, 3] has no duplicates, so uniq! makes no modification and, per its documented behavior, returns nil instead of the array. That nil becomes remove_duplicates‘s implicit return value. Prefer the non-mutating version when you plan to use the method’s return value directly — uniq (no bang) always returns a new array, changed or not:
def remove_duplicates(arr)
arr.uniq
end
result = remove_duplicates([1, 2, 3])
puts result.inspect
Output:
[1, 2, 3]
As a quick reference, here is how some common bang methods behave when they make no change:
| Method | Mutates in place | Returns nil if unchanged? |
|---|---|---|
uniq! |
Yes | Yes |
compact! |
Yes | Yes |
reject! |
Yes | Yes |
select! / filter! |
Yes | Yes |
sort! |
Yes | No — always returns self |
map! / collect! |
Yes | No — always returns self |
Best Practices
- Let the last expression of a method be the value you intend to return; don’t end a method with a debugging
putsor a call whose return value you don’t actually care about. - Reserve the explicit
returnkeyword for early exits (guard clauses), not for the final line of a method — omitting it there is the idiomatic style current Ruby guides recommend. - Use the endless method form (
def name(args) = expr) for short, single-expression methods; it makes the implicit return especially visible. - When a method’s last line is a bang (
!) method, check whether that method can returnnilon no change (likeuniq!orreject!) before relying on its return value. - If a method’s job is to build and hand back a value, structure its body so a conditional or
caseexpression is the last thing evaluated, rather than assigning to a variable and calling a side-effecting method afterward. - When you genuinely want a method to return nothing meaningful (a setter, a method whose whole point is a side effect), that’s fine — just don’t rely on its return value at the call site, and consider ending it with
nilexplicitly if that intent isn’t otherwise obvious.
Practice Exercises
- Write a method
even?(n)using the endless method syntax that returnstrueifnis even andfalseotherwise, without usingInteger#even?. Hint: use the modulo operator%and compare it to0. - Write a method
grade(score)that usesif/elsif/elseas its only body and implicitly returns"A"for scores 90 and above,"B"for 80–89, and"C"otherwise. Call it with95,85, and60; expected output is three lines:A,B,C. - Take the broken
doubledexample from Common Mistakes and rewrite it so it also filters out any doubled value greater than4, still relying entirely on implicit return (no explicitreturnkeyword). For input[1, 2, 3], the expected output is[2, 4].
Summary
- Every Ruby method returns the value of the last expression it evaluates — no
returnkeyword is required at the end of a method. - Because control-flow constructs like
ifandcaseare themselves expressions, the “last expression” is whichever branch execution actually took, not necessarily the last line of source code. - Endless methods (
def name(args) = expr, Ruby 3.0+) make implicit return explicit in the syntax itself: the single expression is the return value. - Explicit
returnis still needed for early exits, but is redundant — and considered non-idiomatic — on a method’s final line. - Watch for methods whose last line is
puts(always returnsnil),each(always returns the receiver, not transformed values), or a bang method likeuniq!/compact!/reject!(returnsnilwhen nothing changed) — each can silently leak the wrong implicit return value. - Only
nilandfalseare falsy in Ruby, and this applies to implicit return too: a conditional with no matching branch and noelseimplicitly returnsnil.
