skip to content

In Ruby, why does case value.class followed by when String never match, while case value with when String does?

level: middleimportance: should knowfreq 45%

answer

  1. String === String
  2. a class is an instance of Class
  3. drop the .class
  4. arrays in when use ==
  5. splat the list instead

basics

~20 s

when String calls String === subject, which asks whether the subject is a String instance. With case value.class the subject is the class String itself, an instance of Class, so the test is false. Write case value and let Module#=== do the type check.

solid answer

~40 s

`case` calls `when_value === subject`, and `Module#===` answers "is the subject an instance of this class or a descendant?". With `case value.class`, the subject is the class object `String`, which is an instance of `Class`, not of `String` — so `String === String` is `false` and the branch never runs. `when Class` would match, which shows what is happening. The fix is to drop `.class` and write `case value` with `when String`; that also accepts subclasses, which `value.class == String` would not. A related trap is putting a collection in a `when`: `when [200, 201]` compares the whole array with `Array#==`, so it never matches an Integer. Use `when 200, 201`, a splat `when *CODES`, or in Ruby 4.0 a `Set` constant, whose `===` is `include?`.

code

ruby · 15 lines
ruby
value = "503"

p(case value.class
  when String then :text
  end)                       # => nil  (String === String is false)

p(case value
  when String then :text
  end)                       # => :text

RETRYABLE = [502, 503, 504]
p(case 503
  when RETRYABLE  then :array     # Array#=== is equality: no match
  when *RETRYABLE then :splatted  # expands to 502, 503, 504
  end)                       # => :splatted

go deeper

for a junior

Remember to write case value, not case value.class, when branching on type, and that when String means the subject is a String.

for a middle

Explain that the class object String is an instance of Class, so String === String is false, and apply the same receiver reasoning to arrays in when, which only compare by equality.

for a senior

Recognise both traps in review, prefer kind-of matching that accepts subclasses and modules, and use splats, ranges or Set constants for membership.

for a principal

When type switches spread through a codebase, weigh whether polymorphic methods or a registry would express the domain better than more case branches.

## The trap Code ported from a language with type switches often looks like this: ```ruby case value.class when String then :text when Integer then :number end # => nil for "503" and for 503 ``` Nothing ever matches. The reason is entirely in how `case` calls `===`. ## What the case actually asks For `when String`, Ruby evaluates `String === subject`. `String` is a class, so this calls `Module#===`, which Ruby documents as returning true "if obj is an instance of mod or an instance of one of mod's descendants". | Code | Subject | Test performed | Result | |---|---|---|---| | `case "503"` / `when String` | `"503"` | `String === "503"` | `true` | | `case "503".class` / `when String` | `String` | `String === String` | `false` | | `case "503".class` / `when Class` | `String` | `Class === String` | `true` | | `case "503".class` / `when String.class` | `String` | `Class === String` | `true`, but for every class | The second row is the bug: the class object `String` is an **instance of `Class`**, not an instance of `String`. The third row proves it — `when Class` matches — and the fourth shows why "fixing" it with `when String.class` is worse: `String.class` is `Class`, so that branch matches any class at all. ## The fix and why it is better Drop the `.class`: ```ruby case value when String then :text when Integer then :number end ``` Benefits over comparing classes by hand: - **Subclasses match.** A subclass of `String` still counts as text, just as `kind_of?` would say. `value.class == String` would reject it. - **Modules work.** `when Comparable` or `when Enumerable` matches anything that includes the module; a class comparison cannot express that. - **It reads as intent**: "what kind of thing is this?". If you genuinely hold a class object and want to branch on it (for example, a class passed as a parameter), compare classes directly with `==` in an `if` — but that is rare; usually the `.class` call is simply unnecessary. ## The sibling trap: collections in when The same "who is the receiver?" rule explains another common bug: ```ruby RETRYABLE = [502, 503, 504] case status when RETRYABLE then retry_later # never matches an Integer end ``` `RETRYABLE === 503` calls `Array#===`, which Arrays do not override — it is the default equality test, so it asks whether the array equals `503`. Three fixes: 1. **Splat the array**: `when *RETRYABLE` expands it into three separate `when` values. 2. **List the values**: `when 502, 503, 504`. 3. **Use a Set**: `when RETRYABLE_SET` works because `Set#===` is an alias of `include?`. In Ruby 4.0 `Set` is a core class, so no `require` is needed. A `Range` constant works for the same reason as a Set — `Range#===` tests membership by bounds — which is why `when 500..599` is idiomatic while `when [500, 501, ...]` is not. ## Checklist for a case that never matches - Is the subject what you think it is? A `.class`, `.to_s` or `.first` on the subject changes what `===` receives. - Does the `when` value's class override `===` the way you expect? Arrays and Hashes do not; Classes, Ranges, Regexps, Procs and Sets do. - Is an earlier, broader `when` catching the value first? - Is there an `else`? Without one, the unmatched case silently returns `nil`.

  • Why is `case value` with `when String` better than `value.class == String`?
    `Module#===` performs a kind-of test, so instances of subclasses of `String` also match, and `when` can name modules such as `Comparable` that a class comparison cannot express. `value.class == String` accepts only direct instances and silently rejects subclasses.
  • What happens with `when Set[502, 503, 504]` in Ruby 4.0?
    It matches when the subject is one of those numbers, because `Set#===` is an alias of `Set#include?`. In Ruby 4.0 `Set` is a core class, so no `require` is needed. Define the set as a constant rather than inline, so it is not rebuilt each time the `case` runs.

saying these in an interview costs you the question

  • case value.class with when String is the idiomatic Ruby type switch.
  • Writing when String.class fixes the trap and still distinguishes strings from integers.
  • Arrays override === to check membership, so when [200, 201] matches 200.
  • Module#=== matches only direct instances, not instances of subclasses.
  • String === String is true because a class is equal to itself.