skip to content

In Ruby, how does a case/when expression decide which branch runs, and what value does the whole case return?

level: juniorimportance: must knowfreq 66%

answer

  1. the when value is the receiver
  2. === not ==
  3. top to bottom, first truthy wins
  4. subject evaluated once
  5. no match and no else gives nil

basics

~20 s

Ruby evaluates the case subject once, then for each when value in order calls when_value === subject; the first truthy result selects that branch. The case returns that branch's last expression, the else branch if nothing matched, or nil without an else.

solid answer

~50 s

`case subject` evaluates the subject once, then walks the `when` clauses top to bottom. For each value listed after `when` it calls `value === subject` — the `when` value is the receiver, so the *pattern* decides what matching means. The first truthy result runs that branch and skips everything after it, including remaining values on the same line. Several values can share a `when`, separated by commas, and a splatted array (`when *CODES`) expands into values. Like `if`, `case` is an expression: it returns the last expression of the branch that ran, the `else` branch when nothing matched, or `nil` when nothing matched and there is no `else`. Because the default `Object#===` behaves like `==`, plain literals compare by equality, while classes, ranges, regexps and procs override `===` to match by type, containment, pattern or predicate.

code

ruby · 12 lines
ruby
def classify(status)
  case status
  when 200, 204 then :ok
  when 300..399 then :redirect
  when 500..    then :server_error
  end
end

p classify(204)   # => :ok
p classify(302)   # => :redirect
p classify(503)   # => :server_error
p classify(404)   # => nil (no match, no else)

go deeper

for a junior

Explain that each when value is tested with === against the subject, the first match wins, and the case returns that branch's value or nil when nothing matches and there is no else.

for a middle

Stress that the when value is the receiver of ===, so its class decides matching; show several values per when, a splatted array, then for one-liners and lazy evaluation of later values.

for a senior

Guard against the silent nil from an unmatched case in code paths that feed lookups or alerts, and choose between case, subjectless case and a hash lookup deliberately.

for a principal

Treat case dispatch on types and ranges as part of the domain model: decide when a table-driven or polymorphic design should replace growing case statements across a codebase.

## The shape of a case/when ```ruby label = case status when 200, 204 then "ok" when 301, 302 then "redirect" when 500..599 then "server error" else "other" end ``` Ruby evaluates this in a fixed order: 1. Evaluate the **subject** (`status`) exactly once. 2. Take the first `when` clause and evaluate its first value. 3. Call **`value === subject`** — the `when` value is the receiver, the subject is the argument. 4. If the result is truthy, run that clause's body and **stop**. Otherwise try the next value on the same `when`, then the next `when`. 5. If no value matched, run the `else` body if there is one. The Ruby language reference describes it the same way: "the first `when` that matches is executed and all other matches are ignored", and conditions on one `when` are tried "in turn". ## Why === and why the receiver matters `===` is called the **case equality** operator, but it really means "does this pattern describe that value?". Its default, defined on `Kernel`, behaves like `==` (with an identity shortcut), so literal integers, strings and symbols compare by equality. Many core classes override it: | When value | Method called | Matches when | |---|---|---| | `200` (Integer) | `Integer#===` | `200 == subject` | | `"GET"` (String) | `String#===` | string equality | | `Integer` (a class) | `Module#===` | subject is an instance of `Integer` or a subclass | | `500..599` (Range) | `Range#===` | subject lies between begin and end | | `/timeout/i` (Regexp) | `Regexp#===` | the regexp finds a match in the subject | | `->(c) { c >= 500 }` | `Proc#===` | the proc, called with the subject, returns truthy | Because the **pattern is the receiver**, `when Integer` asks `Integer === subject` — "is the subject an Integer?". Reversing it (`subject === Integer`) would ask something entirely different, which is why `===` is not symmetric and why `case` always puts the `when` value on the left. ## Several values, splats and then - **Several values per `when`** are separated by commas and tried left to right: `when 301, 302, 307`. - **A splat** expands an array into values: `when *RETRYABLE_CODES`. Ruby's own test suite exercises `when *%w/. +/`. - **`then`** separates the values from a body on the same line: `when 404 then "not found"`. Without `then`, the body starts on the next line. - Values after the first match are **not evaluated**, so side effects in later `when` expressions may never happen. ## What case returns `case` is an expression, like `if`: - the value of the **last expression of the branch that ran**; - the value of the **`else` branch** when nothing matched; - **`nil`** when nothing matched and there is no `else`. ```ruby kind = case 418 when 200..299 then :success when 500..599 then :failure end kind # => nil ``` That `nil` is the same trap as an `if` without `else`: a monitoring bot that does `ALERTS.fetch(kind)` will get `KeyError`, and one that does `kind.to_s.upcase` will silently print an empty string. Add an `else` whenever `nil` is not a meaningful answer. ## Subjectless case `case` can also be written without a subject; each `when` then holds a boolean condition, and the first truthy one wins, like `if`/`elsif`: ```ruby case when latency_ms > 2_000 then :page when error_rate > 0.05 then :warn else :ok end ``` No `===` is involved here — each condition is simply evaluated for truthiness. RuboCop's `Style/EmptyCaseCondition` (enabled by default) prefers `if`/`elsif` for this shape. ## Summary - The subject is evaluated once; `when` values are tried top to bottom, left to right. - Each test is `when_value === subject`, so the pattern's class defines matching. - The first truthy match wins; nothing after it is evaluated. - `case` returns the chosen branch's last value, the `else` value, or `nil`.

  • Are the values in later when clauses evaluated if an earlier one matches?
    No. Ruby evaluates `when` values lazily, in order, and stops at the first truthy `===`. Values on the same `when` after the matching one, and all later clauses, are never evaluated, so any method calls or side effects in them do not run.
  • How do you match a subject against every element of an array constant in one when?
    Splat it: `when *RETRYABLE_CODES` expands the array into separate values, each tried with `===`. Writing `when RETRYABLE_CODES` without the splat compares the whole array to the subject with `Array#==`, which is false for an Integer subject. A `Range` or `Set` constant also works without a splat, because their `===` tests membership.

Each when value is a doorman with his own rule, and the subject is the guest walking past: the class doorman checks the guest's kind, the range doorman checks the guest's number, the regexp doorman reads the name. The guest never judges the doorman; the doorman asks the question, which is why the when value is the receiver of ===.

saying these in an interview costs you the question

  • case/when compares the subject with each when value using ==.
  • Ruby calls subject === when_value, so the subject decides how matching works.
  • Every when clause is evaluated and the last matching branch wins.
  • A case with no matching when and no else raises an error.
  • A subjectless case still calls === on each when condition.