skip to content

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%

answer

  1. shape, not just equality
  2. binds variables while matching
  3. values still use ===
  4. in and when never mix
  5. NoMatchingPatternError without else

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.

solid answer

~40 s

`case/when` asks one question per branch: does `when_value === subject` hold? `case/in` matches the **shape** of the subject: array patterns, hash patterns and find patterns, nested to any depth, with bare names binding the matched parts to local variables. Leaf values inside a pattern (`String`, `1..10`, `/bot/`, `"message"`) are still tested with `===`, so everything you know from `when` carries over. The two keywords cannot be mixed in one `case`. The big behavioural difference is exhaustiveness: a `case/in` with no matching branch and no `else` raises `NoMatchingPatternError`, whereas a `case/when` just returns `nil`. For webhook payloads parsed from JSON that means an unknown event type fails loudly at the dispatch point unless you add an explicit `else`.

code

ruby · 20 lines
ruby
require "json"

def route(event)
  case event
  in {type: "message", user: {id: String => user_id}, text:}
    "reply to #{user_id}: #{text}"
  in {type: "reaction_added", reaction:}
    "count #{reaction}"
  end
end

body  = '{"type":"message","user":{"id":"U42"},"text":"hi"}'
event = JSON.parse(body, symbolize_names: true)
p route(event)                        # => "reply to U42: hi"

begin
  route({type: "channel_created"})
rescue NoMatchingPatternError => e
  p e.class                           # => NoMatchingPatternError
end

go deeper

for a junior

Explain that case/in matches the structure of arrays and hashes and binds parts to variables, and that with no match and no else it raises NoMatchingPatternError.

for a middle

Contrast the === test of case/when with structural patterns, show value patterns still using === inside a hash pattern, and state that in and when cannot be mixed.

for a senior

Use the raise-on-no-match default deliberately in dispatchers such as webhook routers, and add an explicit else only where unknown input is expected and handled.

for a principal

Weigh case/in against polymorphic handlers or a registry for event dispatch, considering how visible, testable and exhaustive each keeps the list of supported event types.

## Two different questions Ruby has two `case` forms that look similar but ask different questions. | | `case/when` | `case/in` | |---|---|---| | Each branch tests | `when_value === subject` | a **pattern** against the subject's structure | | Can look inside arrays/hashes | no | yes, to any depth | | Binds variables | no | yes | | No match, no `else` | returns `nil` | raises `NoMatchingPatternError` | | Guards | no (write the condition in the body) | `in pattern if cond` / `unless cond` | The Ruby language reference notes that `in` and `when` branches "can NOT be mixed in one case expression". ## What a pattern can be Per the reference, a pattern is one of: - a **value pattern** — any Ruby object, matched with `===` exactly as in `when` (`Integer`, `200..299`, `/bot/`, `"message"`); - an **array pattern** `[a, b, *rest]`, which matches arrays (or objects with `deconstruct`); - a **find pattern** `[*, x, *]`, which searches inside an array; - a **hash pattern** `{type:, user: {id:}}`, which matches hashes (or objects with `deconstruct_keys`); - an **alternative** `p1 | p2`; - a **variable** or **as-pattern** `pattern => name`, which binds. ## A webhook router A chat service posts JSON events; after parsing with symbol keys they are plain hashes and arrays: ```ruby case event in {type: "message", user: {id: String => user_id}, text:} reply(user_id, text) in {type: "reaction_added", reaction:} count(reaction) in {type: "ping"} :pong end ``` One branch checks the event type, verifies that `user.id` is a String, **and** extracts `user_id` and `text` — work that with `case/when` would take a type check, several `dig` calls and a few assignments. Key mechanics: 1. **Top to bottom.** Branches are tried in order; the first matching pattern (whose guard, if any, also passes) wins. 2. **Binding happens during matching.** `text:` in a hash pattern binds a local named `text`; `String => user_id` checks with `===` and then binds. 3. **Leaf checks are `===`.** That is why `String`, ranges and regexps work inside patterns. 4. **Bound names are ordinary locals** of the enclosing method, visible after the `case`. The reference leaves the value of variables from a branch that did *not* match undefined, so never read them outside the branch that matched. ## Exhaustiveness: the error instead of nil The reference calls `case/in` *exhaustive*: "if the value of the expression does not match any branch of the case expression (and the else branch is absent), NoMatchingPatternError is raised". For the router above, a `channel_created` event raises: ```ruby route({type: "channel_created"}) # NoMatchingPatternError ``` This is usually what you want in a dispatcher — an unhandled event type is a bug to surface, not a `nil` to pass along. When unknown events are expected, say so: ```ruby else log_unhandled(event) end ``` `NoMatchingPatternError` is a `StandardError`, so a bare `rescue` catches it. Its subclass `NoMatchingPatternKeyError` appears when a single-branch match fails on a missing hash key. ## When to choose which - **`case/when`**: one value compared against constants, classes, ranges or regexps — status codes, types, simple dispatch. - **`case/in`**: nested data whose **shape** matters and whose parts you need — parsed JSON, AST nodes, tuples, value objects. - Prefer `case/in` when silent `nil` on an unknown input would be a bug, because its default is to raise. ## Summary `case/in` extends `case` from "does this value `===` that one?" to "does this structure look like that, and give me its parts". Its value tests are still `===`, it binds locals as it matches, it cannot be mixed with `when`, and it raises `NoMatchingPatternError` rather than returning `nil` when nothing matches and there is no `else`.

  • Can a single case expression contain both in and when branches?
    No. Ruby's language reference states that `in` and `when` branches cannot be mixed in one `case` expression; the parser rejects it. If some branches only need `===` tests, write them as value patterns in `in` clauses, since value patterns use `===` too.
  • Are variables bound in a pattern visible after the case expression?
    Yes, they are ordinary local variables of the enclosing scope. But the reference marks the value of variables from patterns that did not match as undefined, so only rely on the names bound by the branch that actually matched, and only inside or after that branch.

saying these in an interview costs you the question

  • case/in returns nil like case/when when no pattern matches and there is no else.
  • Patterns compare leaf values with ==, so classes and ranges cannot be used inside them.
  • You can mix in and when branches in one case to combine both styles.
  • case/in only matches arrays and hashes, never plain values like integers.
  • NoMatchingPatternError is not a StandardError, so a bare rescue will not catch it.