Ruby keyword: case
case subject when pat[, pat]* [then] body [when ...]* [else body] end | case when expr [then] body ... end Ruby’s case keyword picks a branch by comparing a subject against a list of patterns, or by running a list of boolean tests when there is no subject at all. It is the readable alternative to a long if/elsif/else chain, and it is what most other languages would call switch. Like if, case is an expression in Ruby, which means it returns a value instead of running as a bare statement.
The part that distinguishes case from if/elsif is the matching. The subject form of case calls === on each when pattern, sometimes called “case equality”. That is what lets case match against classes, regular expressions, and ranges for free, with no extra code.
Syntax forms
case has two block forms. Both close with end. Both return the value of the last expression in the branch that ran, or nil if no branch ran and there is no else.
Simple case with subject
The most common shape pairs a single subject with one or more when patterns.
n = 2
case n
when 1 then "one"
when 2 then "two"
else "other"
end
# => "two"
The first when whose pattern matches wins, and the rest are not even evaluated. The whole expression returns "two" because that is the last expression in the matching branch.
No-subject form
Omit the subject and case becomes a flat chain of independent boolean tests, equivalent to if/elsif/else. No === is involved; each when clause is just a boolean check.
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"
Reach for this form when you have several independent conditions, not a single value to compare against. It is shorter than an if/elsif/else chain and avoids the temptation to nest a fresh if inside every else.
Multiple values in one when
Separate several patterns with commas inside a single when. Ruby tries them left to right, and the first one that matches wins.
n = 3
case n
when 1, 2, 3 then "small"
when 4, 5, 6 then "medium"
else "large"
end
# => "small"
Order matters when the patterns are not all equivalent. A regex with side effects listed before a string literal will run the regex first; if it matches, the string literal is never even evaluated. Keep cheap and unambiguous patterns first.
then is optional on a newline
then separates the pattern list from the body when they share a line. On its own line the body is unambiguously the next expression, so the separator becomes optional.
case n
when 1, 2, 3
"small"
when 4, 5, 6
"medium"
else
"large"
end
# => "small"
Both forms compile to the same thing. The newline form is what almost every Ruby style guide recommends, including the Ruby community style guide.
else is optional
Without an else, a case that finds no match returns nil. There is no hidden else nil clause, just the same default an empty method body would produce.
case 99
when 1 then "one"
end
# => nil
Adding else makes the fallback explicit. Some RuboCop configs enable Style/MissingElse to flag case blocks without an else, but the cop is off by default and the rule is a style choice, not a language rule.
How matching works: ===
The subject form of case runs pattern === subject for each when. Most classes inherit === as ==, so plain values match with normal equality. A few core classes override === for richer matching, and that is where case becomes more useful than a stack of == checks.
Object#=== is ==
By default, === is just ==. If you override == on a class but leave === alone, both work the same way inside case.
case 42
when String then "string"
when Integer then "integer"
end
# => "integer"
For plain values like 1, "foo", or :bar, === reduces to ==, so case reads like a regular switch.
Module#=== checks is_a?
Module overrides === to call is_a?, which makes class matching a one-liner.
obj = [1, 2, 3]
case obj
when Array then "array"
when Hash then "hash"
when Enumerable then "enumerable"
end
# => "array"
This is the most common reason Ruby developers prefer case over if/elsif/else when the dispatch is really about type.
Regexp#=== runs a match
Regexp overrides === to call match?, so a regex pattern in a when clause tests whether the subject matches.
line = "ERROR: disk full"
case line
when /^ERROR/ then "log_error"
when /^WARN/ then "log_warn"
else "log_info"
end
# => "log_error"
The regex is matched, not captured. If you need the match data, Regexp.last_match (or $~, $1, and friends) is still populated after the match.
Range#=== uses cover?
Range overrides === to call cover?, which is faster than include? for numeric ranges because it does not allocate intermediate values.
score = 87
case score
when 90..100 then "A"
when 80..89 then "B"
when 70..79 then "C"
else "F"
end
# => "B"
For non-numeric ranges that lack a natural succ, cover? and include? agree, so case matches the way you would expect.
Proc#=== calls the proc
Proc overrides === to invoke the proc with the subject as its argument. It is a handy trick for predicates.
positive = ->(n) { n > 0 }
case 5
when positive then "yes"
else "no"
end
# => "yes"
Method objects behave the same way. A method object used in a when clause runs as if you had called it on the subject.
Case expression return values
Because case is an expression, the result can be assigned, returned, or passed to another method. The value is the last expression evaluated in the matched branch, or nil if no branch ran and there is no else.
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
A method that would be a chain of return statements in other languages reads as a single value expression here. Once you internalize that case returns the value of its last branch, you stop reaching for temporary variables and explicit returns, and the method body becomes a single expression that picks the right value to hand back.
Pattern matching with case/in
A separate pattern-matching form, case expr in pattern ... end, was added in Ruby 2.7 as an experimental feature and promoted out of experimental status in Ruby 3.0. It uses a different matching mechanism — deconstructing arrays, hashes, and custom classes against shapes rather than calling === — so it deserves its own reference page. The rest of this article covers the classic case/when form.
Common gotchas
when evaluates pattern === subject, not subject === pattern
This asymmetry catches people who assume === is symmetric. Putting a String literal in a when will not match a Symbol, because String#=== is String#==, and == does not equate "foo" with :foo.
sym = :foo
case sym
when "foo" then "string match"
when :foo then "symbol match"
end
# => "symbol match"
Put the more specific matcher on the right. When the subject is a Symbol, write a Symbol literal in the when (or a Regexp, Range, or Proc that knows what to do with a symbol).
Patterns are evaluated each entry
Patterns in when clauses are not constants. They are re-evaluated every time the case runs. Anything with side effects in a pattern runs every time, which is almost always a bug.
case x
when Time.now.hour then "now-ish"
end
# Time.now.hour is read every time case runs, not once at parse time
Compute the value ahead of time if you want it stable across the match.
else is not the same as a final when true
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 0 then "zero"
when false then "false"
else "no match"
end
# => "no match"
If you write when true as the catch-all, you skip both nil and false. Most catch-all branches should be else.
then with a comma list applies to the whole list
When several patterns share a then, the separator marks the boundary between the entire comma-separated list and the body. There is no per-pattern then.
case n
when 1, 2, 3 then "small"
end
The body "small" runs whenever any of 1, 2, or 3 matches the subject, not just when 3 matches.
No elsif in case
There is no elsif keyword inside a case block. Add another when.
case n
when 1 then "one"
elsif n == 2 then "two" # SyntaxError: unexpected keyword 'elsif'
end
Use one when per condition, or move to a no-subject case for independent boolean tests.
No-subject case is sugar for if/elsif/else
When the subject is omitted, each when clause is a boolean expression, not a === test. Reach for the no-subject form when the conditions are independent checks rather than variations on a single comparison.
case
when user.admin? && user.active? then "full access"
when user.active? then "limited access"
else "guest"
end
This is shorter than the equivalent if/elsif/else chain and reads better when there is no shared subject.
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 to the next when. If you want several branches to share code, write the same body for each, or hoist the shared work outside the case.
Idiomatic patterns
case shines when the dispatch is data-driven: a request class, a log line, a numeric bucket, or a chain of independent predicates. The subsections below walk through the most common shapes that show up in real codebases, with a short example for each and notes on the footguns that come with them.
Match by class
When the dispatch is really about type, case with module patterns reads more cleanly than is_a? chains. Rails controllers lean on this heavily for handling different request subclasses, and the same trick works for any place where polymorphism is doing the heavy lifting underneath.
case request
when Net::HTTP::Get then handle_read(request)
when Net::HTTP::Post then handle_write(request)
end
Order still matters here: subclasses should appear before their parents. A Net::HTTP::Get is still an Object, so a when Object branch placed earlier would swallow the request before the more specific handlers ever run. If the type hierarchy gets deep, reach for polymorphism with define_method or a strategy object instead.
Match by range
Numeric bucketing is a perfect fit for case. Letter grades, age brackets, HTTP status families, and discount tiers all decompose into ranges, and the code reads as a lookup table from top to bottom.
case status
when 200..299 then :ok
when 300..399 then :redirect
when 400..499 then :client_error
when 500..599 then :server_error
end
The branches are evaluated in source order, so the most specific range goes first. Overlapping ranges are a real footgun here: a value of 250 would match the first 200..299 branch and never reach a later 200..500, so plan the boundaries so they are exclusive or sort them by intent.
Match by regex
Regex patterns keep log classification and command dispatch readable. Each when reads as a rule, and the whole block becomes a self-documenting dispatcher that you can scan at a glance.
case command
when /^restart/ then restart_service
when /^status/ then print_status
when /^stop/ then stop_service
end
For longer matchers, prefer $~ or Regexp.last_match(1) to pull out captured groups. Anchoring matters: ^restart is a totally different rule from restart, and forgetting the anchor is the kind of bug that hides in production until someone passes a command like force_restart.
No-subject form for multi-condition booleans
When you have several independent predicates, the no-subject form lays them out flat. Each when is its own boolean check, so it is easy to read, comment, and reorder without accidentally breaking the chain.
case
when user.nil? then redirect_to_login
when user.banned? then render_banned_page
when user.suspended? then render_suspended_page
else render_dashboard
end
This shape is also a good place to short-circuit early. Put the cheap conditions first so the expensive database lookups never run when the cheap ones already short-circuit the dispatch. If the conditions get long or start to share state, that is usually a sign the logic wants to move into a dedicated policy object.