skip to content

Case/In Patterns

case/in destructures arrays and hashes by shape, with pins, guards and alternatives, and the one-line in and => forms. Interviewers probe what raises on no match and how your own classes opt in.

on this pageshow

explore

questions

6

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.
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

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 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