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 key
  • SyntaxError: pattern matching is experimental on Ruby 2.7
  • Code parses fine on Ruby 3.1 but fails on Ruby 3.0
  • NoMatchingPatternError raised 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/in on Ruby 2.7 (experimental, requires enable pragma)
  • Mixing when and in clauses in the same case statement
  • Missing pin operator ^ for variable references in patterns
  • Using case instead of in for array destructuring
  • Pattern guard clause syntax incorrect (if placement wrong)

Step-by-Step Fix

  1. 1.Check Ruby version compatibility:
  2. 2.```bash
  3. 3.ruby -v
  4. 4.# Pattern matching stable in Ruby 3.0+
  5. 5.# For Ruby 2.7, add pragma at top of file:
  6. 6.# # frozen_string_literal: true
  7. 7.# # enable pattern matching
  8. 8.`
  9. 9.Fix case/in syntax - do not mix with when:
  10. 10.```ruby
  11. 11.# WRONG - mixing when and in
  12. 12.case order
  13. 13.when order.pending?
  14. 14.puts "pending"
  15. 15.in { status: "shipped" } # SyntaxError!
  16. 16.puts "shipped"
  17. 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. 1.Use pin operator for variable references:
  2. 2.```ruby
  3. 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. 1.Fix guard clause placement:
  2. 2.```ruby
  3. 3.# WRONG - guard clause in wrong position
  4. 4.case data
  5. 5.in { type: "user", name: } if name.present? # SyntaxError
  6. 6.puts name
  7. 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. 1.Handle NoMatchingPatternError at runtime:
  2. 2.```ruby
  3. 3.# Add else clause or rescue
  4. 4.case order_data
  5. 5.in { id:, total: }
  6. 6.Order.create(id: id, total: total)
  7. 7.in _
  8. 8.Rails.logger.warn("Unmatched order format: #{order_data.inspect}")
  9. 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-version and CI configuration
  • Use rubocop with Lint/DuplicateCaseCondition rule enabled
  • Write specs covering all pattern branches with diverse input shapes
  • Avoid pattern matching in code shared across Ruby version boundaries
  • Use in only when the data structure shape is guaranteed
  • Consider using case/when with explicit checks for simpler logic