skip to content

Control Structures

How Ruby branches and repeats: if and unless as expressions, case/when with ===, case/in pattern matching, while and until loops, and break, next and catch/throw.

on this pageshow

explore

questions

27

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.
open as a page

In Ruby, what value does an if or unless expression return, including when no branch runs, and how do you use it?

level: juniorimportance: must knowfreq 62%

basics

~20 s

Ruby's if and unless are expressions: they return the value of the last expression evaluated in the branch that ran, and nil when no branch runs, so the whole conditional can be assigned to a variable or returned from a method.

open as a page

In Ruby, how do the x if y and x unless y statement modifiers work, and when should you prefer them over a full if block?

level: juniorimportance: must knowfreq 58%

basics

~20 s

A modifier puts the condition after the statement: Ruby evaluates the condition first and runs the statement only if it is truthy (if) or falsy (unless). Use it for a one-line body with a short condition, especially guard clauses.

open as a page

In Ruby, what is the difference between `break` and `next` inside an `each` or `map` block, and what value can `next` carry?

level: juniorimportance: must knowfreq 62%

basics

~20 s

In a Ruby block, next ends only the current call of the block and its argument (nil by default) becomes the block's result; break ends the whole iteration and makes the method that yielded, such as each or map, return immediately.

open as a page

In Ruby, why is `for x in list` discouraged in favour of `list.each`, and what does the for loop leave behind?

level: juniorimportance: must knowfreq 68%

basics

~20 s

Ruby's for...in calls each on the collection but opens no new scope, so its loop variable and body locals survive the loop and every closure made inside shares one variable; each gives every iteration a fresh block parameter.

open as a page

In a Ruby case/when, what do Class#===, Range#===, Regexp#=== and Proc#=== each test, and how would one case use them together?

level: middleimportance: must knowfreq 58%

basics

~20 s

Class#=== (from Module) tests instance-of-or-subclass, Range#=== tests whether the value lies between begin and end, Regexp#=== tests for a match in a string, and Proc#=== calls the proc. One case can mix them to classify input by type, range, pattern and predicate.

open as a page

In Ruby, what does a `while` loop, an `each` call or a `map` call return when `break` ends it early, and when it completes?

level: middleimportance: must knowfreq 48%

basics

~20 s

A break ends a Ruby loop or iterator call with break's argument, or nil for a bare break. Run to completion, while and until give nil, each and times give their receiver, and map gives the new array.

open as a page

In Ruby, when should you use a case with several values per when, a subjectless case, or a hash lookup instead of an if/elsif chain?

level: juniorimportance: should knowfreq 42%

basics

~20 s

Use case/when when many branches test the same subject, listing alternatives with commas. A subjectless case is a boolean chain that RuboCop prefers as if/elsif. A pure value-to-value mapping reads best as a hash lookup.

open as a page

In Ruby, when is the ternary cond ? a : b a good fit, and why are nested or multi-line ternaries discouraged?

level: juniorimportance: should knowfreq 45%

basics

~20 s

Ruby's ternary cond ? a : b is a compact if/else expression that returns a or b. It fits a short condition choosing between two short values; nesting or spreading it over lines hurts readability, so use if/elsif or case instead.

open as a page

In Ruby, when is unless the clearer choice, and why do style guides and RuboCop's Style/UnlessElse reject unless with an else branch?

level: juniorimportance: should knowfreq 48%

basics

~20 s

unless runs its body when the condition is falsy, so it reads well for one simple negative test. Adding else makes the reader negate twice to see when each branch runs, so the convention is to rewrite it as if with the positive case first.

open as a page

In Ruby, how do Integer#times, upto and downto replace a counting loop, and what does each call return?

level: juniorimportance: should knowfreq 55%

basics

~10 s

Ruby counts with methods on Integer: n.times yields 0 to n-1, a.upto(b) and a.downto(b) yield every integer between the two, inclusive. With a block each returns its receiver; without one each returns an Enumerator.

open as a page

In Ruby, how does case/in pattern matching differ from case/when, and what happens when no in branch matches?

level: juniorimportance: should knowfreq 45%

basics

~20 s

case/in matches a value's structure — arrays, hashes and nested data — and binds parts to local variables, while case/when only tests each value with ===. If no in branch matches and there is no else, case/in raises NoMatchingPatternError instead of returning nil.

open as a page

In Ruby, why does case value.class followed by when String never match, while case value with when String does?

level: middleimportance: should knowfreq 45%

basics

~20 s

when String calls String === subject, which asks whether the subject is a String instance. With case value.class the subject is the class String itself, an instance of Class, so the test is false. Write case value and let Module#=== do the type check.

open as a page

In Ruby, when nested `each` blocks search a warehouse's aisles and bins, how do you stop every level at the first matching bin?

level: middleimportance: should knowfreq 40%

basics

~20 s

A Ruby break ends only the innermost each, so outer levels keep going. Wrap the loops in catch(:found) and throw :found, bin, extract them into a method that returns the bin, or rewrite the search as a single find.

open as a page

In Ruby, how does `begin ... end while cond` differ from the `stmt while cond` modifier, and why does RuboCop's Lint/Loop flag it?

level: middleimportance: should knowfreq 38%

basics

~20 s

A while or until modifier on an ordinary statement tests first, so the statement may never run; on a begin...end block it tests after, so the body runs at least once. Lint/Loop flags that special case and suggests loop with break.

open as a page

In Ruby pattern matching, why does a hash pattern match extra keys while an array pattern must match the whole array, and how do rest patterns change that?

level: middleimportance: should knowfreq 40%

basics

~20 s

An array pattern must account for every element, so [Integer, Integer] rejects a three-element array unless you add *rest. A hash pattern checks only the keys it names, so extras are ignored; **nil forbids them, **rest collects them, and {} matches only an empty hash.

open as a page

In Ruby, how do the one-line expr in pattern and expr => pattern forms differ, and when is NoMatchingPatternKeyError raised instead of NoMatchingPatternError?

level: middleimportance: should knowfreq 36%

basics

~20 s

expr in pattern returns true or false and never raises on a mismatch; expr => pattern binds and raises. A single-pattern match that fails on a missing hash key raises NoMatchingPatternKeyError, a NoMatchingPatternError subclass with key and matchee.

open as a page

A Ruby parking-permit predicate ends in an if with no else, and an API now returns null instead of false — why, and how would you fix it?

level: seniorimportance: should knowfreq 30%

basics

~20 s

A Ruby method returns its last expression, and an if without else evaluates to nil when its test fails. nil passes truthiness checks but serialises as null and fails == false, so return a real boolean via a guard clause or an explicit else.

open as a page

In Ruby, how do `catch`/`throw` differ from `raise`/`rescue`, and what happens when `throw` finds no matching `catch`?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Ruby's throw is non-error unwinding to the catch with the same tag object, handing it a value and running ensure clauses; rescue never sees it. With no matching catch, throw raises UncaughtThrowError, an ArgumentError subclass.

open as a page

A greenhouse controller's `loop do` that polls a flaky sensor stops with no error and no log line — how can Ruby's Kernel#loop cause that?

level: seniorimportance: should knowfreq 26%

basics

~10 s

Kernel#loop rescues StopIteration raised anywhere in its block and returns that exception's result, so an exhausted enumerator deep in the polling code ends the loop as if it had finished normally.

open as a page

How do you make your own Ruby class matchable by array and hash patterns, and what should deconstruct_keys do with its keys argument?

level: seniorimportance: should knowfreq 28%

basics

~20 s

Define deconstruct (returning an Array) for array and find patterns and deconstruct_keys(keys) (returning a Hash) for hash patterns. keys lists the Symbols the pattern needs, or is nil when it uses **rest, so you may compute only those keys.

open as a page

In a Ruby case/in, what does the ^ pin operator do, and what goes wrong when an existing local variable is used without it?

level: seniorimportance: should knowfreq 34%

basics

~20 s

In a pattern a bare name always binds a new value, so using an existing local like team_id silently overwrites it and matches anything. The pin ^team_id uses the variable's current value as a value pattern (tested with ===) instead; ^@ivar and ^(expr) work too.

open as a page

In Ruby, why does p a if a = lookup raise NameError when a is new, while x = 1 if false leaves x as nil?

level: middleimportance: nice to knowfreq 22%

basics

~20 s

Ruby decides at parse time, in source order, whether a bare name is a local. In p a if a = lookup the body is parsed first, so a is a method call; x = 1 if false still declares x as nil.

open as a page

In Ruby, what does `redo` do inside a `while` loop or an iterator block, and why can it spin forever?

level: middleimportance: nice to knowfreq 14%

basics

~10 s

Ruby's redo restarts the current iteration from the top of the body without re-testing the loop condition or advancing to the next element; if whatever triggered it never changes, the same pass repeats forever.

open as a page

In Ruby, why does `1.0.step(2.0, 0.1)` end exactly at 2.0 when a `while` loop adding 0.1 each pass misses it?

level: middleimportance: nice to knowfreq 20%

basics

~20 s

For floats, Numeric#step computes the number of values up front and yields start + i * step, clamping an overshoot to the limit, so rounding error never accumulates; a while loop that keeps adding 0.1 drifts and skips 2.0.

open as a page

How do you make your own Ruby objects usable as when values, and what correctness and performance traps come with a custom ===?

level: seniorimportance: nice to knowfreq 24%

basics

~20 s

Define === on the pattern object, or use a lambda, Method or Set, whose === already works. Keep it total so wrong input returns false, and build matchers once as constants, since when values are re-evaluated on every run.

open as a page

In Ruby case/in, how do the find pattern, | alternatives and if/unless guards work, and which of them may bind variables?

level: seniorimportance: nice to knowfreq 24%

basics

~20 s

A find pattern [*, p, *] matches a run of elements anywhere in an array and can bind them. Alternatives p1 | p2 accept either shape but cannot bind (except _names). Guards (in p if cond) run after binding, only on case/in branches.

open as a page