skip to content

Methods & Arguments

How Ruby methods are defined, what they return, how positional, splat and keyword parameters bind, and how calls chain. Interviewers use it to test the 3.0 keyword split.

on this pageshow

explore

questions

22

In Ruby, what does a method return when it has no return keyword, and when is an explicit return still worth writing?

level: juniorimportance: must knowfreq 78%

answer

  1. whatever ran last
  2. if with no branch taken: nil
  3. puts gives nil, each gives self
  4. guard clause exits early
  5. return a, b builds an Array

basics

~20 s

A Ruby method returns the value of the last expression it actually evaluated, so no return keyword is needed. Explicit return earns its place for early exits such as guard clauses; return a, b returns one Array.

solid answer

~40 s

Every Ruby method returns the value of the **last expression evaluated** in its body, along whichever branch actually ran. That is why an `if` with no matching branch yields `nil`, a trailing `puts` yields `nil`, and a trailing `each` yields its receiver rather than anything computed in the block. `return` is still worth writing for an **early exit**: a guard clause such as `return "n/a" if @celsius.nil?` keeps the happy path unindented, and a bare `return` gives `nil`. `return a, b` does not return two objects; it packs them into one `Array` the caller can destructure. A `return` on the last line changes nothing, and RuboCop's `Style/RedundantReturn` flags it.

code

ruby · 18 lines
ruby
class Temperature
  def initialize(celsius)
    @celsius = celsius
  end

  def both_scales
    return @celsius, @celsius * 9 / 5.0 + 32
  end

  def print_readings(readings)
    readings.each { |r| puts r }
  end
end

temp = Temperature.new(20)
temp.both_scales                # => [20, 68.0]
c, f = temp.both_scales         # c = 20, f = 68.0
temp.print_readings([18, 21])   # prints 18 and 21, returns [18, 21]

go deeper

for a junior

Recall the rule in one line: the last evaluated expression is the return value. Then name two traps, an if with no branch taken giving nil and puts returning nil.

for a middle

Explain why each returns its receiver, how return a, b becomes one Array that callers destructure, and why guard clauses are the main legitimate use of return.

for a senior

Show how an escaping nil from an unguarded if surfaces far from its cause, and argue for explicit else branches or guard clauses on public methods whose callers rely on the value.

for a principal

Weigh positional multi-value returns against small value objects for public APIs, and decide which RuboCop return-style settings a team enforces so reviews stop debating them.

## The last evaluated expression is the return value In Ruby almost everything is an **expression** that produces a value: `if`, `case`, `begin`, assignments and method calls all evaluate to something. A method body is a sequence of expressions, and when the body finishes, the method hands back the value of the **last expression that was evaluated**. That rule has two parts, and both matter: - **Last** means the final expression on the path the code took, not the final line of source text. - **Evaluated** means a branch that did not run contributes nothing; the enclosing `if` or `case` evaluates to `nil` instead. So `def one_plus_one = 1 + 1` and a three-line `def ... end` whose final line is `1 + 1` both return `2`, and neither needs the `return` keyword. ## Last lines that surprise people Most "my method returned the wrong thing" bugs come from a last line whose value is not what the author pictured: | Last expression in the body | What the method returns | |---|---| | `if` / `elsif` with no branch taken and no `else` | `nil` | | `puts "done"` | `nil` (`puts` always returns `nil`) | | `@readings.each { ... }` | the receiver, `@readings` (`Array#each` returns `self`) | | `@total = @total + reading` | the assigned value | | `case` with no matching `when` and no `else` | `nil` | The `each` row is the classic interview trap: the block's values are thrown away, and the method returns the collection it iterated. When the transformed values are the point, the last expression should be `map` (or another method that returns them), not `each`. ## When an explicit return earns its place The `return` keyword ends the method immediately with the value given, or with `nil` when none is given. Idiomatic Ruby uses it for: 1. **Guard clauses** at the top of a method, such as `return "n/a" if @celsius.nil?`, so the main logic below is not wrapped in an `if`. 2. **Leaving a loop early** from inside the method once the answer is known. 3. **Returning several values** with `return a, b` (see below). On the final line, `return x` behaves exactly like `x`. RuboCop's `Style/RedundantReturn` cop, enabled by default, reports it and autocorrects it away. ```ruby class Temperature def initialize(celsius) @celsius = celsius end def label return "n/a" if @celsius.nil? # guard clause if @celsius >= 30 "hot" elsif @celsius <= 0 "freezing" end # 15 => nil: no branch ran end end Temperature.new(35).label # => "hot" Temperature.new(15).label # => nil Temperature.new(nil).label # => "n/a" ``` ## Why Ruby leans on implicit return Ruby treats a method body as an expression that produces a value, the same way `if` and `case` do. That has practical effects: - Short methods read as a description of their result: `def freezing? = @celsius <= 0` needs nothing else. - A conditional can be the last expression and supply the value directly, which avoids temporary variables such as `result = ...` followed by `return result`. - Blocks follow the same rule: the value a block hands back to `map`, `select` or `sort_by` is its last expression, which is why `temps.map { |t| t.celsius * 2 }` needs no keyword. The flip side is that every last line is a return value whether you meant it or not. A method written for its side effect, such as logging a reading, still returns something, and callers can start depending on it. Ending such methods with a deliberate `nil` or `self`, and documenting which, keeps an accidental contract from forming. ## Returning several values A Ruby method always returns exactly **one object**. Writing `return @celsius, fahrenheit` builds an `Array` of the listed values and returns that, so `Temperature.new(20).both_scales` can return `[20, 68.0]`. The caller then unpacks it with multiple assignment, `c, f = temp.both_scales`. A few details follow from the Array rule: - `return *list` splats a list into the returned Array, and an empty list returns `[]`. - `return [a, b]` and `return a, b` return equal Arrays; the second form is just shorter syntax. - `Style/RedundantReturn` has an `AllowMultipleReturnValues` option, `false` by default, for teams that prefer `return a, b` over a bare `[a, b]` on the last line. For more than two or three values, a named value object communicates better than a positional Array, because the caller no longer has to remember which slot holds which value. ## How to reason about a method's value - Trace the path the call actually takes and find the last expression evaluated on it. - Treat every `if` without an `else` as a place where `nil` can escape. - Remember which common methods return `nil` (`puts`, `print`, `p` with no arguments) or their receiver (`each`, `each_with_index`). - Keep `return` for early exits, where it makes intent clearer, rather than for decoration on the last line.

  • In Ruby, what does a method whose last line is `@readings.each { |r| puts r }` return?
    It returns `@readings` itself, because `Array#each` returns its receiver once it finishes. The `puts` calls inside the block return `nil`, but their values are discarded by `each`. If the caller needs transformed values, the last expression should be `map` or another method that returns them.
  • If a Ruby method can only ever return one object, how does `return 20, 68.0` hand back two values?
    It builds the Array `[20, 68.0]` and returns that single object. The caller can destructure it with multiple assignment, `c, f = temp.both_scales`, which makes it look like two return values. `return *list` works the same way and returns `[]` for an empty list.
  • What does a bare `return` with no value give back in a Ruby method, and when is it idiomatic?
    A bare `return` ends the method and returns `nil`. It is idiomatic in guard clauses of methods called for their side effects, where the caller ignores the value, such as `return if readings.empty?` at the top of a method that writes a report.

saying these in an interview costs you the question

  • A Ruby method without a return statement returns nil.
  • An if whose condition fails returns false as the method's value.
  • return a, b hands the caller two separate objects, like a tuple on the stack.
  • A method ending in each returns the values its block produced.
  • The last line needs return, or the value is lost.
  • puts at the end of a method returns the string it printed.
open as a page

In Ruby, what does `def send_email(to:, subject: "(no subject)", cc: [])` declare, and what happens when a caller omits `to:` or adds `bcc:`?

level: juniorimportance: must knowfreq 72%

basics

~20 s

The signature declares one required keyword, to:, and two optional keywords, subject: and cc:, that fall back to their defaults. Omitting to: raises ArgumentError (missing keyword: :to); an unlisted bcc: raises ArgumentError (unknown keyword: :bcc).

open as a page

In Ruby, what does `def log(*messages)` collect, and how do `log(lines)` and `log(*lines)` differ when lines is an Array?

level: juniorimportance: must knowfreq 70%

basics

~10 s

*messages gathers every leftover positional argument into a new Array, empty when there are none. log(lines) passes one argument, so messages is [lines]; log(*lines) splats the Array into separate arguments first.

open as a page

In Ruby, why does `title.strip!.squeeze!(" ")` raise NoMethodError for some product titles but not others, and how do you rewrite it?

level: middleimportance: must knowfreq 64%

basics

~20 s

String#strip! returns nil when there was no surrounding whitespace to remove, so the next link calls squeeze! on nil. Titles that needed stripping pass; clean ones crash. Chain the non-bang forms, or call each bang method as its own statement.

open as a page

In Ruby, what is the difference between `Kernel#tap` and `Kernel#then` in a method chain, and when would you use each?

level: middleimportance: must knowfreq 55%

basics

~20 s

Both yield the receiver to a block. tap returns the receiver itself, ignoring the block's value, so it suits side effects such as logging mid-chain; then (alias yield_self) returns the block's value, so it transforms or pipes the value onward.

open as a page

In Ruby 3.0 and later, why does `send_email(settings)` raise ArgumentError when `settings` is a Hash and `send_email` declares keyword parameters?

level: middleimportance: must knowfreq 66%

basics

~20 s

Since Ruby 3.0 a Hash passed as an ordinary argument stays positional and is never converted into keywords, so a method expecting only keywords sees one surplus argument. Pass it as send_email(**settings) to splat it into keywords.

open as a page

In Ruby, how do you split a long method chain across lines so it stays one expression, and where does a block attach?

level: juniorimportance: should knowfreq 40%

basics

~20 s

Start each continuation line with a dot, or end the previous line with one; either tells the parser the expression continues. A block attaches to the call written just before it, and the chain may continue after its closing brace or end.

open as a page

In Ruby, when does omitting parentheses on a call, or putting a space before them, change which object a chained method runs on?

level: middleimportance: should knowfreq 35%

basics

~20 s

Without parentheses, everything after the method name is its argument list, so format_title raw.upcase upcases raw, not the result. A space before the parenthesis makes it a grouping inside the argument, so format_title (raw).upcase behaves the same way; format_title(raw).upcase chains on the result.

open as a page

In Ruby, how do the alias keyword and Module#alias_method differ, and when do you need alias_method instead of alias?

level: middleimportance: should knowfreq 52%

basics

~20 s

alias is a keyword taking bare names and aliasing in the lexically enclosing class; it also aliases globals. alias_method is a Module method taking Symbol or String arguments, acting on self at runtime and returning the new name.

open as a page

In Ruby 4.0, what does `def fahrenheit = celsius * 9 / 5.0 + 32` define, and what does that def expression itself return?

level: middleimportance: should knowfreq 45%

basics

~20 s

It defines an instance method fahrenheit whose body is that one expression; endless defs arrived in Ruby 3.0. The def expression itself evaluates to the Symbol :fahrenheit, which is why private def fahrenheit = ... works.

open as a page

In Ruby, how do you define +, unary minus and [] on a Temperature class, and which operators can you not define?

level: middleimportance: should knowfreq 42%

basics

~20 s

Operators are methods: def +(other), def -@ for unary minus and def . Binary operators take one argument, and += comes free from +. Logical operators like && and ||, plain = and compound assignment cannot be defined.

open as a page

In Ruby, why does `temp.celsius = 30.46` evaluate to 30.46 even when the celsius= method rounds and returns 30.5?

level: middleimportance: should knowfreq 38%

basics

~10 s

Ruby's assignment syntax, recv.name = value or recv[k] = value, always evaluates to the right-hand value and discards what the name= method returned. Calling it through public_send(:celsius=, 30.46) returns the method's own value.

open as a page

In Ruby, what do a `**opts` parameter and a `**nil` parameter each do with the keyword arguments a caller passes?

level: middleimportance: should knowfreq 50%

basics

~20 s

A **opts parameter gathers every keyword the method does not name into a new Hash, so unknown keywords stop raising. A **nil parameter declares that the method takes no keywords: passing any raises ArgumentError (no keywords accepted).

open as a page

In Ruby, why prefer keyword parameters over a trailing `options = {}` Hash parameter for a method with many optional email settings?

level: middleimportance: should knowfreq 48%

basics

~20 s

Keyword parameters declare every setting and its default in the signature, reject misspelled names, enforce required ones at the call, and bind false correctly. An options Hash silently accepts typos, needs hand-written defaults, and may be the caller's own mutable object.

open as a page

In Ruby 4.0, what do `wrong number of arguments (given 1, expected 2..3)` and `(given 0, expected 1+)` tell you about the method called?

level: middleimportance: should knowfreq 38%

basics

~20 s

Given is the number of positional arguments passed; expected is the accepted range. 2..3 means two required plus one optional; 1+ means one required and a rest parameter. The backtrace's top frame is the called method's definition.

open as a page

In Ruby, given `def log(level, prefix = "app", *messages, suffix)`, how does the call `log(:info, "a", "b")` bind its three arguments?

level: middleimportance: should knowfreq 40%

basics

~20 s

level gets :info and the trailing required suffix gets "b"; the one argument left fills prefix with "a", and messages is []. Ruby fills required parameters at both ends first, then optionals left to right, then the rest.

open as a page

In Ruby, when is a default such as `def log(msg, at = Time.now, tags = [])` evaluated, and may it use earlier parameters?

level: middleimportance: should knowfreq 45%

basics

~20 s

Ruby evaluates a default expression on each call that omits that argument, left to right, so every call gets a fresh Time.now and a new []. A default may use earlier parameters, but passing nil explicitly does not trigger it.

open as a page

In Ruby 3.x, why does a wrapper that forwards only `*args` break keyword arguments, and what does `ruby2_keywords` change about that?

level: seniorimportance: should knowfreq 30%

basics

~20 s

A method with only *args has no keyword parameters, so incoming keywords become a positional Hash and are forwarded as one. ruby2_keywords flags that Hash so a later *args splat passes it as keywords again; Ruby-3-only code should forward **kwargs explicitly instead.

open as a page

In Ruby 4.0, how do anonymous `*`, `**` and `&` parameters and `...` forward a logging helper's arguments, and what can each form not do?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Anonymous *, ** and & pass positional arguments, keywords or the block on without naming them; ... forwards all three together. Anonymous parameters cannot be read, and ... cannot be split or joined by a literal block.

open as a page

A ReadOnlyTemperature subclass must stop responding to an inherited celsius= writer; in Ruby, why does remove_method fail there while undef_method works?

level: seniorimportance: nice to knowfreq 22%

basics

~20 s

remove_method deletes only a method the class itself defines, so on an inherited writer it raises NameError, and even when it succeeds lookup falls through to the parent. undef_method blocks the name in that class, so calls raise NoMethodError.

open as a page