skip to content

In Ruby, when should you use a case with several values per when, a subjectless case, or a hash lookup instead of an if/elsif chain?

level: juniorimportance: should knowfreq 42%

answer

  1. same subject, many comparisons
  2. commas, not ||
  3. case with no subject
  4. Style/CaseLikeIf, Style/HashLikeCase
  5. then only on one-liners

basics

~20 s

Use case/when when many branches test the same subject, listing alternatives with commas. A subjectless case is a boolean chain that RuboCop prefers as if/elsif. A pure value-to-value mapping reads best as a hash lookup.

solid answer

~40 s

Reach for `case subject` when three or more branches test the same value: `when 301, 302, 307` lists alternatives without repeating `status ==` and `||`, and the pattern classes (ranges, classes, regexps) come for free. RuboCop's `Style/CaseLikeIf` flags `if`/`elsif` chains of three or more branches that compare the same thing and could be a `case`. A subjectless `case` with boolean `when` conditions is legal, but `Style/EmptyCaseCondition` (enabled) prefers plain `if`/`elsif` for it. When every branch just maps a literal to a literal, `Style/HashLikeCase` suggests a hash constant instead, looked up with `fetch` so a missing key is loud. Use `then` only to put a short body on the same line as its `when`; `Style/MultilineWhenThen` flags it on multi-line bodies. Add an `else` whenever an unmatched value should not quietly become `nil`.

code

ruby · 16 lines
ruby
REDIRECTS = [301, 302, 307, 308].freeze

def severity(status)
  case status
  when 200, 204      then :none
  when *REDIRECTS    then :info
  when 400..499      then :warn
  when 500..599      then :page
  else :unknown
  end
end

LEVELS = { debug: 0, info: 1, warn: 2, error: 3 }.freeze

p severity(307)          # => :info
p LEVELS.fetch(:warn)    # => 2

go deeper

for a junior

Show a case with several comma-separated values per when and an else, and explain that it replaces a chain of == and || tests on the same variable.

for a middle

Compare case with a subject, subjectless case, if/elsif and a hash lookup, and name the RuboCop cops that steer each choice, including why CaseLikeIf is unsafe to autocorrect.

for a senior

Choose between a case and a data-driven hash for maintainability, insist on loud failure for unknown keys, and catch unreachable duplicate when values in review.

for a principal

Set team conventions for multi-way dispatch, such as whether subjectless case is allowed and when mappings must live in data, and enforce them through RuboCop configuration.

## Three shapes for multi-way choice Ruby gives you three idiomatic tools for "pick one of many", and choosing well is mostly about what the branches have in common. | Shape | Use when | Example | |---|---|---| | `case subject` / `when` | many branches test the **same value** | `case status when 200..299 ...` | | subjectless `case` | branches test **unrelated conditions** | `case when cpu > 90 ... when disk < 5 ...` | | `if` / `elsif` | a few unrelated conditions | `if cpu > 90 ... elsif disk < 5 ...` | | hash lookup | every branch **maps a value to a value** | `SEVERITY.fetch(level)` | ## case with a subject `case` shines when one subject is compared many times: ```ruby severity = case status when 200, 204 then :none when 301, 302, 307 then :info when 400..499 then :warn when 500..599 then :page else :unknown end ``` - **Several values per `when`** replace `status == 301 || status == 302 || status == 307`; each value is tried with `===` in order. - **A splat** (`when *REDIRECTS`) turns an array constant into values. - **Pattern classes** mean ranges, classes and regexps work without extra method calls. - RuboCop's **`Style/CaseLikeIf`** (enabled; `MinBranchesCount: 3`) flags an `if`/`elsif` chain that repeatedly compares the same expression and suggests this rewrite. It is marked unsafe for autocorrection because `==` and `===` can disagree for some objects. ## The subjectless case Without a subject, each `when` holds an ordinary condition and the first truthy one wins; no `===` is involved: ```ruby case when cpu_percent > 90 then :page when disk_free_gb < 5 then :warn else :ok end ``` This is exactly an `if`/`elsif` chain written with different keywords. RuboCop's **`Style/EmptyCaseCondition`** (enabled) flags it and prefers `if`/`elsif`, which keeps `case` for the "one subject, many patterns" shape. Some teams like the aligned look for long rule tables; if you keep it, be consistent. ## When a hash beats both If every branch is a literal-to-literal mapping, the code is really data: ```ruby SEVERITY = { debug: 0, info: 1, warn: 2, error: 3 }.freeze SEVERITY.fetch(level) # KeyError for an unknown level ``` **`Style/HashLikeCase`** (enabled; `MinBranchesCount: 3`) flags a `case` whose branches all compare with literals and return literals. A hash constant is easier to extend, can be loaded from configuration, and `fetch` fails loudly on an unknown key, whereas a `case` without `else` returns `nil` silently. Keep the `case` when branches do real work, use ranges or classes, or need ordering. ## then, else and layout 1. **`then`** puts a short body on the same line as its `when` (`when 404 then :missing`). On multi-line bodies it is noise, and `Style/MultilineWhenThen` flags it. `Style/WhenThen` flags the old `when x; body` semicolon form and prefers `then`. 2. **`else`** handles everything else. Omit it only when `nil` is a correct result; `Style/EmptyElse` flags an `else` that is empty or holds only `nil`. 3. **Duplicates**: listing the same literal in two `when` clauses makes the second unreachable. `Lint/DuplicateCaseCondition` (enabled) catches it, and Ruby itself warns about a duplicated `when` clause when warnings are enabled with `-w`. 4. **Empty branches**: `Lint/EmptyWhen` flags a `when` with no body, which usually means a forgotten implementation. ## Deciding in an interview - One subject, several comparisons → `case subject`. - Unrelated conditions → `if`/`elsif` (or a subjectless `case` if the team accepts it). - Pure value mapping → hash constant with `fetch`. - Always decide consciously whether an unmatched value should be `nil`, an `else` value, or an error.

  • Why does RuboCop mark Style/CaseLikeIf as unsafe to autocorrect?
    The rewrite turns `x == value` tests into `when value`, which calls `value === x`. For most literals `===` and `==` agree, but for classes, ranges, regexps, procs or custom objects they can differ, so the automatic rewrite could change which branch runs. The cop documents itself as unsafe for exactly that reason, and its config sets `Safe: false`.
  • Why prefer `SEVERITY.fetch(level)` over a case/when mapping without else?
    An unmatched `case` without `else` returns `nil` silently, and the `nil` surfaces later as a confusing error. `Hash#fetch` raises `KeyError` naming the missing key at the point of lookup. The hash is also data: easy to extend, freeze or load from configuration.

saying these in an interview costs you the question

  • Several values on one when must all match for the branch to run.
  • A subjectless case compares each when condition with === against true.
  • A literal-to-literal case/when is always clearer than a hash lookup.
  • then is required after every when value list.
  • Ruby raises an error when two when clauses list the same literal.