rubyguides

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.

See also