Implicit Return

Published by Programming Line in Ruby. Last updated .

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 of last_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:

  1. Ruby calls classify with n bound to 0.
  2. The method body contains one top-level expression: the if/elsif/else. Ruby begins evaluating it.
  3. n.negative? is checked first: 0.negative? is false, so that branch is skipped.
  4. n.zero? is checked next: 0.zero? is true, so Ruby evaluates that branch’s body, the string literal "zero", and the else branch is never reached.
  5. The whole if expression’s value is now "zero" — that is what an if/elsif/else expression evaluates to: the value of whichever branch actually ran.
  6. Since that if was the last expression evaluated in classify‘s body, the method returns "zero" to its caller.
  7. 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 puts or a call whose return value you don’t actually care about.
  • Reserve the explicit return keyword 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 return nil on no change (like uniq! or reject!) 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 case expression 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 nil explicitly if that intent isn’t otherwise obvious.

Practice Exercises

  • Write a method even?(n) using the endless method syntax that returns true if n is even and false otherwise, without using Integer#even?. Hint: use the modulo operator % and compare it to 0.
  • Write a method grade(score) that uses if/elsif/else as its only body and implicitly returns "A" for scores 90 and above, "B" for 80–89, and "C" otherwise. Call it with 95, 85, and 60; expected output is three lines: A, B, C.
  • Take the broken doubled example from Common Mistakes and rewrite it so it also filters out any doubled value greater than 4, still relying entirely on implicit return (no explicit return keyword). 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 return keyword is required at the end of a method.
  • Because control-flow constructs like if and case are 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 return is 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 returns nil), each (always returns the receiver, not transformed values), or a bang method like uniq!/compact!/reject! (returns nil when nothing changed) — each can silently leak the wrong implicit return value.
  • Only nil and false are falsy in Ruby, and this applies to implicit return too: a conditional with no matching branch and no else implicitly returns nil.