Ruby keyword: when
when expr [, expr]* [then] body The when keyword introduces a branch clause inside a case expression. Each when carries one or more patterns, the case runtime picks the first pattern that matches the subject, and only that branch’s body runs. when never appears on its own; it has meaning only as a clause of case, and case defines the surrounding rules. The case reference page covers the enclosing expression. This page focuses on the clause itself: its syntax, its matcher, the then and else keywords it lives with, and the gotchas that surface in real code.
Syntax
A when clause takes one or more comma-separated patterns, an optional then, and a body.
n = 3
case n
when 1, 2, 3 then "small"
when 4, 5, 6 then "medium"
else "large"
end
# => "small"
The body can sit on the same line (separated by then) or on the next line (no separator needed). then is purely cosmetic; both forms compile to the same code.
case n
when 1, 2, 3
"small"
when 4, 5, 6
"medium"
else
"large"
end
Patterns in a comma list are tried left to right. The first one that matches wins, and the remaining patterns in that when are not even evaluated. Patterns are not constants, so anything with side effects in a pattern runs every time the case runs. Compute the value once outside the case if you need a stable comparison.
Matching with ===
The subject form of case runs pattern === subject for each when. Most classes inherit === as ==, so plain values match with normal equality. The few classes that override === for richer matching are where when becomes more useful than a stack of == checks.
Module#=== calls is_a?, which makes class dispatch a one-liner.
obj = [1, 2, 3]
case obj
when Array then "array"
when Hash then "hash"
when Enumerable then "enumerable"
end
# => "array"
Regexp#=== calls match? against the subject, so a when /^foo/ branch tests whether the pattern matches anywhere in the string. Match data is still populated in $~ and Regexp.last_match after a successful match, which means you can read captures out of the regex from inside the same clause if you need them.
line = "ERROR: disk full"
case line
when /^ERROR/ then "log_error"
when /^WARN/ then "log_warn"
else "log_info"
end
# => "log_error"
Range#=== calls cover?, which is faster than include? for numeric ranges because it does not enumerate the range. That makes ranges the natural fit for grade buckets and tier checks, since you only need the endpoints and never actually walk the values. A when 90..100 clause evaluates in constant time regardless of how wide the range is.
score = 87
case score
when 90..100 then "A"
when 80..89 then "B"
when 70..79 then "C"
else "F"
end
# => "B"
Proc#=== invokes the proc with the subject as its argument. Method#=== behaves the same way: a method object used in a when clause runs as if you had called it on the subject.
positive = ->(n) { n > 0 }
case 5
when positive then "yes"
else "no"
end
# => "yes"
Symbol and String do not override ===, which is the source of a common trap. case sym; when "foo"; end will not match the symbol :foo, because String#=== falls back to String#==.
Overriding === on your own classes
You can define === on your own classes to teach when a domain-specific match. Defining the method on the class itself (as a singleton method) shadows the inherited Module#===, so the case runtime calls your version instead.
class Status
def self.===(other)
%i[ok ready running].include?(other)
end
end
case job_state
when Status then "active"
else "idle"
end
Whatever your === returns (truthy or falsy) decides the match. This is the same hook Module, Regexp, Range, and Proc use to make when more expressive than a plain == check.
when is not in
Since Ruby 2.7, case has a second form that uses in instead of when. The two forms share the case keyword but use entirely different matchers and have different semantics.
case/when | case/in | |
|---|---|---|
| Introduced | Ruby 1.0 | Ruby 2.7 (unflagged in 3.0) |
| Matcher | pattern === subject | Shape-based deconstruction |
| Can destructure arrays and hashes | No | Yes |
| Binds variables in the pattern | No | Yes (in [x, y] binds both) |
| Hash partial match | N/A | Default behavior |
data = { user: "alice", role: :admin }
case data
in {role: :admin}
"admin user"
in {role: :guest}
"guest"
else
"other"
end
# => "admin user"
A single case cannot mix when and in; the parser disallows it. If you want destructuring or variable binding, reach for case/in. If you want ===-based matching against classes, regexes, ranges, or procs, stay with case/when.
No-subject case
case does not require a subject. Without one, each when body is evaluated as a plain boolean expression, and Ruby picks the first one that is truthy. In that form === is not involved at all.
n = 7
case
when n.even? && n > 0 then "positive even"
when n.even? then "non-positive even"
when n.odd? then "odd"
end
# => "odd"
This shape is useful when each branch tests a different condition rather than the same shape of value. Once you find yourself stacking more than a few of these, an if/elsif chain usually reads better, since the no-subject case does not benefit from === and gains only the then/else syntax sugar.
Return value
The whole case expression returns the value of the matched branch’s last expression. when itself is a clause, not a value, but the surrounding case behaves like any other Ruby expression, so you can use the result directly.
def fee(age)
case age
when 0..11 then 0
when 12..64 then 10
else 5
end
end
fee(8) # => 0
fee(30) # => 10
fee(70) # => 5
If no when matches and there is no else, the whole case returns nil. That silent default trips people up when they assume an unmatched subject will raise. Add an explicit else (even an empty one with a comment) when the missing branch is meaningful and you want to spot it during review.
Common gotchas
=== is not symmetric
when evaluates pattern === subject, not subject === pattern. The pattern always sits on the left of ===. This asymmetry catches people who assume the operator is reflexive.
sym = :foo
case sym
when "foo" then "string match"
when :foo then "symbol match"
end
# => "symbol match"
String === :foo reduces to String#== between "foo" and :foo, which is false. Symbol === :foo is true.
when true is not the same as else
else runs whenever no when matched, including when the subject is nil or false. A when true would only fire when the subject is exactly true.
case nil
when true then "true"
else "fallback"
end
# => "fallback"
If you want a catch-all, write else. Reaching for when true as a default will silently skip nil and false.
Patterns are re-evaluated each entry
Patterns in when clauses are not memoized. Anything with side effects in a pattern runs every time the case runs.
case x
when Time.now.hour then "now-ish"
end
# Time.now.hour is read on every case evaluation, not once at parse time
Hoist the value above the case if you need it stable across the match.
No fall-through between when clauses
Each when is its own branch and the first match wins. Unlike C’s switch, there is no implicit fall-through into the next when. To share a body across values, write the same body for each branch or hoist the work above the case.
case n
when 1 then "one"
when 2 then "one" # separate branch, not a fall-through
end
If n is 1, only the first when runs.