Introduction
Ruby 3.0 introduced stable pattern matching with the case/in syntax, but using it incorrectly or on older Ruby versions causes SyntaxError at parse time. Pattern matching is a powerful feature for destructuring arrays, hashes, and objects, but the syntax differs from traditional case/when and has strict requirements that can trip up developers migrating from Ruby 2.x.
Symptoms
SyntaxError: unexpected keyword 'in', expecting keyword 'when'SyntaxError: cannot find the deconstructed keySyntaxError: pattern matching is experimentalon Ruby 2.7- Code parses fine on Ruby 3.1 but fails on Ruby 3.0
NoMatchingPatternErrorraised at runtime instead of expected match
Example error:
``
app/services/order_parser.rb:15: syntax error, unexpected keyword 'in', expecting keyword 'when' (SyntaxError)
in { status: "pending", items: [...] }
^~
Common Causes
- Using
case/inon Ruby 2.7 (experimental, requiresenablepragma) - Mixing
whenandinclauses in the samecasestatement - Missing pin operator
^for variable references in patterns - Using
caseinstead ofinfor array destructuring - Pattern guard clause syntax incorrect (
ifplacement wrong)
Step-by-Step Fix
- 1.Check Ruby version compatibility:
- 2.```bash
- 3.ruby -v
- 4.# Pattern matching stable in Ruby 3.0+
- 5.# For Ruby 2.7, add pragma at top of file:
- 6.# # frozen_string_literal: true
- 7.# # enable pattern matching
- 8.
` - 9.Fix case/in syntax - do not mix with when:
- 10.```ruby
- 11.# WRONG - mixing when and in
- 12.case order
- 13.when order.pending?
- 14.puts "pending"
- 15.in { status: "shipped" } # SyntaxError!
- 16.puts "shipped"
- 17.end
# CORRECT - use only in case order in { status: "pending", items: items } process_pending(items) in { status: "shipped", tracking: t } track_shipment(t) in { status: "cancelled", reason: } handle_cancellation(reason) in _ puts "Unknown order format" end ```
- 1.Use pin operator for variable references:
- 2.```ruby
- 3.expected_status = "pending"
# WRONG - 'status' treated as pattern variable binding case order in { status: expected_status } # Binds expected_status, does not compare puts "matched" # Always matches! end
# CORRECT - pin with ^ compares to existing variable case order in { status: ^expected_status } # Compares order[:status] == "pending" puts "matched" end ```
- 1.Fix guard clause placement:
- 2.```ruby
- 3.# WRONG - guard clause in wrong position
- 4.case data
- 5.in { type: "user", name: } if name.present? # SyntaxError
- 6.puts name
- 7.end
# CORRECT - guard after pattern, before body case data in { type: "user", name: } if name.present? puts name end
# Alternative - use find pattern for arrays case [1, 2, 3, 4, 5] in [*, 3, 4, *] puts "Found 3, 4 in sequence" end ```
- 1.Handle NoMatchingPatternError at runtime:
- 2.```ruby
- 3.# Add else clause or rescue
- 4.case order_data
- 5.in { id:, total: }
- 6.Order.create(id: id, total: total)
- 7.in _
- 8.Rails.logger.warn("Unmatched order format: #{order_data.inspect}")
- 9.end
# Or rescue specific error begin case data in { type: "user", name:, email: } User.new(name: name, email: email) end rescue NoMatchingPatternError => e Sentry.capture_exception(e, extra: { data: data }) end ```
Prevention
- Pin Ruby version in
.ruby-versionand CI configuration - Use
rubocopwithLint/DuplicateCaseConditionrule enabled - Write specs covering all pattern branches with diverse input shapes
- Avoid pattern matching in code shared across Ruby version boundaries
- Use
inonly when the data structure shape is guaranteed - Consider using
case/whenwith explicit checks for simpler logic