Skip to content

Nothing matched: case/in raises, case/when returns nil

Level: 201 · anyone who has been surprised by a NoMatchingPatternError in production

One line: A case/in with no matching branch and no else, and a rightward value => pattern, raise NoMatchingPatternError — a StandardError whose message names the failed check, with the subclass NoMatchingPatternKeyError carrying the missing key and its matchee — while the boolean value in pattern and the older case/when never raise; Python's match with no matching case does nothing at all, and raising is an idiom you add.

The two pattern-matching forms that commit to a match — case … in without an else, and expr => pattern — treat "nothing matched" as an error, on the reasoning that a shape you did not anticipate is a bug rather than a value. The exception is NoMatchingPatternError, a StandardError, so a bare rescue catches it; its message in Ruby 4.0 says what was tested and which check failed. When the failed check was a missing hash key, the subclass NoMatchingPatternKeyError is raised instead, and it carries the key that was missing and the matchee — the hash that was being examined, which for a nested pattern is the inner hash. The two forms that merely ask — value in pattern, which returns a boolean, and case/when, which returns nil when no when matched — never raise, and an else on a case … in turns the raise into a value. The program raises each way and reads the exceptions.

Verified output of no_matching_pattern_error_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

 1. case 5; in String (no else)              -> NoMatchingPatternError: 5: String === 5 does not return true
 2. {a: 1} => {b:}                           -> NoMatchingPatternKeyError: {a: 1}: key not found: :b; key = :b, matchee = {a: 1}
 3. case {a: 1}; in {b:}                     -> NoMatchingPatternKeyError: {a: 1}: key not found: :b
 4. 5 => String                              -> NoMatchingPatternError: 5: String === 5 does not return true
 5. [1, 2] => [a]                            -> NoMatchingPatternError: [1, 2]: [1, 2] length mismatch (given 2, expected 1)
 6. 5 in String                              -> false
 7. case 5 when String (no else)             -> nil
 8. ancestors                                -> [NoMatchingPatternError, StandardError, Exception]; NoMatchingPatternKeyError.superclass -> NoMatchingPatternError
 9. rescue => e (bare) on case/in            -> bare rescue caught NoMatchingPatternError
10. case 5; in String ... else               -> "else branch"
11. x = (5 => Integer)                       -> SyntaxError  (void value); 5 => Integer alone -> compiles
12. {a: {b: 1}} => {a: {c:}}                 -> key = :c, matchee = {b: 1}

Reading the output

Row 1: case 5 in String with no else raised NoMatchingPatternError, and the message reads 5: String === 5 does not return true — the subject, then the check that failed. Row 2: {a: 1} => {b:} raised the subclass, NoMatchingPatternKeyError, whose message ends in key not found: :b, and whose key and matchee methods return :b and the hash. Row 3: a case … in with the same missing key raises the same subclass. Rows 4 and 5 are the rightward form failing a class check and a length check, each message saying which: String === 5 does not return true, length mismatch (given 2, expected 1).

Rows 6 and 7 are the two forms that only ask: 5 in String is false, and case 5 when String with no match is nil. Row 8 places the classes: NoMatchingPatternError sits directly under StandardError, and NoMatchingPatternKeyError under it, so row 9's bare rescue => e catches the failed match. Row 10: an else makes the case … in a total function again and nothing is raised. Row 11 is a parsing fact worth knowing before you try it: expr => pattern is a statement with no value — x = (5 => Integer) is a SyntaxError, a "void value expression" — so it cannot be used inside an assignment or an argument, only on a line of its own. Row 12: for a nested pattern the matchee is the inner hash {b: 1}, the one that lacked :c, not the outer document.

Compared with Python

Python's match never raises for a miss: when no case matches, execution continues after the statement as if it were an if whose conditions were all false, and the twin's helper reports nothing ran for rows 1, 2, 3 and 12 — a missing key, a wrong class and a nested miss are all just cases that did not match. Committing to a match is therefore an idiom you write: a final case _: that raises (row 4), or typing.assert_never(v) in that last branch (row 8), which raises AssertionError — and doubles as a signal to type checkers that the earlier cases were meant to be exhaustive (Not machine-checked here.). The one destructuring that raises on its own is ordinary unpacking, a, = [1, 2], which is a ValueError (row 5). The boolean question is isinstance (row 6), an if with no else leaves None (row 7), and case _: is the else (row 10). Row 9: both idiom exceptions are ordinary Exception subclasses. Row 11 agrees with Ruby for a different reason: match is a statement, so x = match … is a SyntaxError.

Verified output of no_matching_pattern_error_py.py — regenerated by tools/run_examples.py, never hand-typed.

 1. case 5; in String (no else)              -> nothing ran
 2. {a: 1} => {b:}                           -> nothing ran
 3. case {a: 1}; in {b:}                     -> nothing ran
 4. 5 => String                              -> silent; `case _: raise` idiom -> ValueError: no pattern matched 5
 5. [1, 2] => [a]                            -> silent as a pattern; `a, = [1, 2]` -> ValueError
 6. 5 in String                              -> False
 7. case 5 when String (no else)             -> None
 8. ancestors                                -> assert_never(5) -> AssertionError: Expected code to be unreachable, but got: 5
 9. rescue => e (bare) on case/in            -> issubclass(AssertionError, Exception) -> True
10. case 5; in String ... else               -> 'else branch'
11. x = (5 => Integer)                       -> SyntaxError for x = match v: ...
12. {a: {b: 1}} => {a: {c:}}                 -> nothing ran; config["a"]["c"] -> KeyError

The Python library has no page on match; the silent-miss rule is in the reference's match statement ↗ section, and assert_never in the typing ↗ docs.

Try it

  1. Write parse(event) as a case/in with two branches and no else, call it with a good event and one of an unknown type, and rescue what the bad one raises, printing its class. Then match the good event with a rightward => that names a key it lacks, and print the exception's key. Finish with the two forms that never raise: bad in {…} and a case/when.
  2. Write validate!(config) as config => {host: String, port: Integer} and provoke its three failure modes — a missing key, a value of the wrong class, and a config that is not a Hash at all — printing each exception's class and message.
Solution to 1

no_matching_pattern_error_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.

# Exercise 1: a parser with no else, called with a good and a bad event; the
# exceptions are rescued and read, then the two forms that never raise.

def parse(event)
  case event
  in {type: "click", x: Integer => x, y: Integer => y} then "click at #{x},#{y}"
  in {type: "key", key: String => key} then "key #{key}"
  end
end

good = {type: "key", key: "q"}
bad = {type: "scroll", delta: 3}
puts "good event -> #{parse(good)}"
begin
  parse(bad)
rescue NoMatchingPatternError => e
  puts "bad event  -> #{e.class}"
end

begin
  good => {type:, modifiers:}
rescue NoMatchingPatternKeyError => e
  puts "=> with a missing key -> #{e.class}, key #{e.key.inspect}"
end

puts "boolean in -> #{(bad in {type: "click"})}"
puts "case/when  -> #{(case bad when Hash then "a hash" end)}"

Verified output of no_matching_pattern_error_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

good event -> key q
bad event  -> NoMatchingPatternError
=> with a missing key -> NoMatchingPatternKeyError, key :modifiers
boolean in -> false
case/when  -> a hash

See also