Overview
Ruby's case/when expressions provide pattern matching capabilities that go beyond simple equality comparisons. The case statement evaluates an expression and compares it against multiple when clauses using the case equality operator (===
), which enables sophisticated matching patterns including ranges, regular expressions, classes, and custom objects.
The case expression returns the value of the matched when clause, making it both a control structure and an expression that can be assigned to variables or used in method chains. Ruby evaluates when clauses sequentially from top to bottom, executing the first match and ignoring subsequent clauses.
def categorize_number(num)
case num
when 0
"zero"
when 1..10
"small positive"
when -Float::INFINITY...-1
"negative"
else
"large positive"
end
end
categorize_number(5) # => "small positive"
categorize_number(-3) # => "negative"
The case equality operator behavior varies by object type. Ranges use include?
, regular expressions use match semantics, classes check inheritance, and procs call themselves with the case expression as an argument. This flexibility makes case statements adaptable to different matching requirements.
case "hello@example.com"
when /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
"valid email"
when String
"string but not email"
else
"not a string"
end
# => "valid email"
Ruby case expressions support multiple values per when clause, implicit array creation with comma separation, and optional else clauses for default behavior. The entire case expression returns nil
if no when clause matches and no else clause exists.
Basic Usage
Case expressions match values using the case equality operator, which provides different comparison semantics than the regular equality operator (==
). This distinction enables flexible pattern matching across different data types and structures.
def process_response(response)
case response
when 200
"success"
when 404, 403, 401
"client error"
when 500..599
"server error"
else
"unknown status"
end
end
process_response(404) # => "client error"
process_response(503) # => "server error"
Regular expressions in when clauses match against string representations of the case expression. Ruby converts the case expression to a string if necessary and applies the regular expression match.
def identify_file_type(filename)
case filename
when /\.rb$/
"Ruby source"
when /\.js$/, /\.ts$/
"JavaScript/TypeScript"
when /\.(jpg|jpeg|png|gif)$/i
"image file"
when /\.txt$/
"text file"
else
"unknown type"
end
end
identify_file_type("script.rb") # => "Ruby source"
identify_file_type("photo.JPG") # => "image file"
Class matching uses inheritance hierarchy, matching the case expression against class types and their ancestors. This enables type-based dispatch and polymorphic behavior.
def handle_exception(error)
case error
when StandardError
"standard error: #{error.message}"
when SystemExit
"system exit with code #{error.status}"
when Exception
"other exception: #{error.class}"
else
"not an exception object"
end
end
handle_exception(ArgumentError.new("bad arg")) # => "standard error: bad arg"
Case expressions work without an explicit case value, functioning like if/elsif chains but with when clause syntax. This pattern suits complex conditional logic where multiple variables require evaluation.
def categorize_user(user)
case
when user.admin?
"administrator"
when user.premium? && user.active?
"premium member"
when user.active?
"regular member"
when user.suspended?
"suspended account"
else
"inactive user"
end
end
Advanced Usage
Advanced case expressions leverage Ruby's case equality operator customization through the ===
method. Custom classes can override this method to define specialized matching behavior for domain-specific pattern matching.
class AgeRange
def initialize(min, max)
@min, @max = min, max
end
def ===(age)
(age >= @min) && (age <= @max)
end
def to_s
"#{@min}-#{@max} years"
end
end
child = AgeRange.new(0, 12)
teen = AgeRange.new(13, 19)
adult = AgeRange.new(20, 64)
senior = AgeRange.new(65, 120)
def life_stage(age)
case age
when child
"childhood"
when teen
"teenage years"
when adult
"adulthood"
when senior
"senior years"
else
"invalid age"
end
end
life_stage(8) # => "childhood"
life_stage(45) # => "adulthood"
Proc objects and lambdas in when clauses call themselves with the case expression as an argument, enabling functional pattern matching and complex conditional logic encapsulation.
positive = ->(n) { n > 0 }
negative = ->(n) { n < 0 }
even = ->(n) { n.even? }
odd = ->(n) { n.odd? }
def number_properties(num)
properties = []
case num
when positive
properties << "positive"
when negative
properties << "negative"
else
properties << "zero"
end
case num
when even
properties << "even"
when odd
properties << "odd"
end
properties.join(" and ")
end
number_properties(6) # => "positive and even"
number_properties(-3) # => "negative and odd"
Complex pattern matching combines multiple techniques, including guard clauses simulated through case expressions without explicit case values and nested case statements for hierarchical matching.
class RequestRouter
def route(request)
case request[:method]
when "GET"
case request[:path]
when "/"
handle_root
when /^\/users\/(\d+)$/
handle_user_show($1.to_i)
when /^\/api\//
case
when request[:headers]["Content-Type"] == "application/json"
handle_api_json(request)
when request[:headers]["Authorization"]
handle_api_auth(request)
else
handle_api_basic(request)
end
else
handle_not_found
end
when "POST"
case request[:path]
when "/users"
handle_user_create
when /^\/users\/(\d+)\/posts$/
handle_post_create($1.to_i)
else
handle_method_not_allowed
end
else
handle_method_not_allowed
end
end
private
def handle_root; "root page"; end
def handle_user_show(id); "user #{id}"; end
def handle_api_json(req); "api json"; end
def handle_api_auth(req); "api auth"; end
def handle_api_basic(req); "api basic"; end
def handle_not_found; "404"; end
def handle_user_create; "create user"; end
def handle_post_create(user_id); "create post for #{user_id}"; end
def handle_method_not_allowed; "405"; end
end
Splat operators and array decomposition work within case expressions, though Ruby treats them as array literals rather than pattern matching destructuring. Complex when clauses can combine multiple matching strategies.
def analyze_data_structure(data)
case data
when Array
case data.size
when 0
"empty array"
when 1
"single element: #{data.first}"
when 2..5
"small array with #{data.size} elements"
else
"large array with #{data.size} elements"
end
when Hash
case
when data.empty?
"empty hash"
when data.keys.all? { |k| k.is_a?(String) }
"string-keyed hash with #{data.size} pairs"
when data.keys.all? { |k| k.is_a?(Symbol) }
"symbol-keyed hash with #{data.size} pairs"
else
"mixed-key hash with #{data.size} pairs"
end
when String, Symbol
"scalar value: #{data}"
when NilClass
"nil value"
else
"unknown type: #{data.class}"
end
end
Common Pitfalls
Case expressions use the case equality operator (===
) rather than regular equality (==
), leading to unexpected behavior when developers assume standard equality comparison. This distinction affects how objects match against when clauses.
# Common mistake: expecting == behavior
string = "42"
number = 42
case string
when number
"matches number" # This will NOT execute
else
"no match" # This executes because 42 === "42" is false
end
# Correct approach: understand === behavior
case string
when /^\d+$/
"numeric string" # This executes
when String
"regular string"
else
"something else"
end
Range matching uses include?
internally, but this creates performance issues with large ranges and unexpected results with non-comparable objects. Developers often assume ranges work identically to mathematical intervals.
# Performance trap with large ranges
huge_range = (1..1_000_000)
# This creates the entire range in memory for some Ruby versions
case 999_999
when huge_range
"found" # Slow and memory-intensive
end
# Better approach for large numeric ranges
def in_range?(value, min, max)
value >= min && value <= max
end
case 999_999
when method(:in_range?).curry[1][1_000_000]
"found" # More efficient but complex
end
# Unexpected behavior with non-comparable objects
case "b"
when ("a".."z") # Works fine
"lowercase letter"
when (Date.today..Date.today + 30) # Type mismatch
"this month" # Never matches strings
end
When clauses execute sequentially, and Ruby stops at the first match, causing issues when broader patterns precede more specific ones. Order dependency creates maintenance problems and logical errors.
# Wrong order: broad pattern first
def categorize_error(error)
case error
when StandardError # Too broad, catches everything first
"standard error"
when ArgumentError # Never reached
"argument error"
when NoMethodError # Never reached
"method error"
else
"other error"
end
end
# Correct order: specific to general
def categorize_error(error)
case error
when NoMethodError # Most specific first
"method error"
when ArgumentError # More specific
"argument error"
when StandardError # Most general last
"standard error"
else
"other error"
end
end
Variable assignment within when clauses creates scoping issues and uninitialized variable errors when the case expression doesn't match expected patterns.
# Problematic variable assignment
def process_data(input)
case input
when Array
result = input.map(&:upcase) # Only assigned if input is Array
when String
result = input.upcase # Only assigned if input is String
end
result.length # NameError if input doesn't match any when clause
end
# Better approach: initialize variables or use return values
def process_data(input)
result = case input
when Array
input.map(&:upcase)
when String
input.upcase
else
[] # Default value
end
result.length
end
Regular expressions in when clauses can produce confusing behavior because Ruby converts the case expression to a string before matching. This implicit conversion causes type-related bugs.
# Confusing implicit conversion
number = 42
case number
when /4/ # number.to_s => "42", then /4/ matches
"contains four" # This executes unexpectedly
end
# Class checking fails with inheritance confusion
class CustomString < String
end
custom = CustomString.new("hello")
case custom
when String # Matches because CustomString inherits from String
"string"
when CustomString # Never reached due to inheritance
"custom string"
end
Multiple values in when clauses use array semantics, but developers sometimes expect logical OR behavior that doesn't account for array edge cases.
# Unexpected array behavior
values = [1, 2, 3]
case values
when 1, 2, 3 # This creates [1, 2, 3] and compares arrays
"matches" # Never executes because [1,2,3] === [1,2,3] is false
end
# Correct approach depends on intent
case values
when Array
if [1, 2, 3].include?(values) || values == [1, 2, 3]
"matches array"
end
end
# Or for individual elements
values.each do |value|
case value
when 1, 2, 3
puts "found #{value}"
end
end
Reference
Case Expression Syntax
Pattern | Syntax | Behavior |
---|---|---|
Basic case | case expr; when value; end |
Compares using === |
Multiple values | when val1, val2, val3 |
Matches any value (OR logic) |
No case expression | case; when condition; end |
Functions like if/elsif |
With else | case expr; when val; else; end |
Default clause if no match |
Assignment | result = case expr; when val; end |
Returns matched clause value |
===
) Behavior
Case Equality Operator (Type | Behavior | Example |
---|---|---|
Range |
Uses include? |
(1..10) === 5 # => true |
Regexp |
Matches against string | /\d+/ === "42" # => true |
Class |
Checks inheritance | String === "hello" # => true |
Proc/Lambda |
Calls with argument | ->(x) { x > 0 } === 5 # => true |
Module |
Checks class/ancestors | Enumerable === Array # => true |
Object |
Falls back to == |
42 === 42 # => true |
Pattern Matching Examples
# Range patterns
case age
when 0...13 then "child"
when 13...20 then "teenager"
when 20...65 then "adult"
when 65..Float::INFINITY then "senior"
end
# Regular expression patterns
case input
when /\A\d+\z/ then "integer"
when /\A\d+\.\d+\z/ then "decimal"
when /\A[a-zA-Z]+\z/ then "letters only"
when /\A\s*\z/ then "whitespace"
end
# Class patterns
case object
when String then object.upcase
when Integer then object * 2
when Array then object.size
when Hash then object.keys
when NilClass then "nothing"
end
# Proc patterns
positive = ->(n) { n > 0 }
even = ->(n) { n.even? }
case number
when positive then "positive"
when even then "even"
end
Performance Characteristics
Pattern Type | Performance | Memory Usage | Notes |
---|---|---|---|
Integer literals | Excellent | Minimal | Direct comparison |
String literals | Very good | Low | Hash table lookup |
Small ranges (< 100) | Good | Low | Array creation |
Large ranges (> 1000) | Poor | High | Avoid if possible |
Regular expressions | Moderate | Moderate | Compilation cost |
Class checking | Very good | Minimal | Ancestor chain walk |
Proc calls | Moderate | Low | Method call overhead |
Error Handling Patterns
# Exception type matching
begin
risky_operation
rescue => error
case error
when ArgumentError
handle_bad_arguments(error)
when StandardError
handle_standard_error(error)
when SystemExit
handle_system_exit(error)
else
handle_unknown_error(error)
end
end
# Validation patterns
def validate_input(input)
case input
when nil, ""
raise ArgumentError, "input cannot be empty"
when String
input.strip
when Numeric
input.to_s
else
raise TypeError, "unsupported input type: #{input.class}"
end
end
Common Return Values
Scenario | Return Value | Example |
---|---|---|
Match found | Value of when clause | "result" |
No match, has else | Value of else clause | "default" |
No match, no else | nil |
nil |
Empty when clause | nil |
when condition; # => nil |
Multiple statements | Last statement value | when val; a = 1; b = 2; b returns 2 |
Complex Matching Strategies
# Nested case expressions
case request
when Hash
case request[:type]
when "GET"
handle_get(request)
when "POST"
case request[:format]
when "json"
handle_post_json(request)
when "xml"
handle_post_xml(request)
end
end
end
# Guard clause simulation
case
when user.nil?
"no user"
when user.admin? && user.active?
"active admin"
when user.admin?
"inactive admin"
when user.active?
"regular user"
else
"inactive user"
end
# Combined pattern types
case data
when /^\d{4}-\d{2}-\d{2}$/, Date
parse_date(data)
when /^\d+$/, Integer
parse_number(data)
when String
parse_string(data)
when Array, Hash
parse_structure(data)
end