skip to content

In Ruby, when is the ternary cond ? a : b a good fit, and why are nested or multi-line ternaries discouraged?

level: juniorimportance: should knowfreq 45%

answer

  1. compact two-way if/else
  2. both branches required
  3. binds looser than ||, tighter than =
  4. ?a is a character literal
  5. Style/NestedTernaryOperator

basics

~20 s

Ruby's ternary cond ? a : b is a compact if/else expression that returns a or b. It fits a short condition choosing between two short values; nesting or spreading it over lines hurts readability, so use if/elsif or case instead.

solid answer

~40 s

`cond ? a : b` is the one-line form of `if cond then a else b end`: it evaluates `cond`, then exactly one of `a` or `b`, and returns that value. It needs both branches — there is no ternary without an else. In Ruby's precedence table `?:` sits below `||` and ranges but above modifier-`rescue` and assignment, so `label = paid ? "ok" : "due"` assigns the chosen string without parentheses. Use it when a short condition picks between two short values. Nested ternaries (`a ? (b ? 1 : 2) : 3`) and ternaries broken across lines are hard to scan; RuboCop's `Style/NestedTernaryOperator` and `Style/MultilineTernaryOperator` (both enabled by default) push them back into `if`/`else` or `case`. Keep spaces around `?` and `:` — `?a` on its own is a character literal.

code

ruby · 19 lines
ruby
Permit = Struct.new(:zone, :resident, :expired)
permit = Permit.new("A", false, false)

status = permit.expired ? "renew" : "ok"
p status                                  # => "ok"

# Nested ternary: flagged by Style/NestedTernaryOperator
fee = permit.resident ? 0 : (permit.zone == "A" ? 40 : 25)

# Clearer
fee =
  if permit.resident
    0
  elsif permit.zone == "A"
    40
  else
    25
  end
p fee                                     # => 40

go deeper

for a junior

Show that cond ? a : b returns a or b, needs both branches, and fits short two-way choices; mention spaces around ? and :.

for a middle

Explain where the ternary sits in the precedence table, why a compound test deserves parentheses, and which cops flag nested and multi-line ternaries.

for a senior

Recognise when a ternary is hiding side effects or a boolean-literal branch, and refactor it into an if, a named predicate or a case for three or more outcomes.

for a principal

Settle ternary style as a team rule — for example parentheses around complex tests — and let RuboCop enforce it so review time goes to logic rather than formatting.

## The ternary is a compact if/else expression Ruby's language reference introduces the ternary as another way to write an if-then-else expression: ```ruby input_type = gets =~ /hello/i ? "greeting" : "other" # is the same as input_type = if gets =~ /hello/i "greeting" else "other" end ``` Key properties: - It is an **expression**: it returns the value of whichever branch ran. - **Exactly one branch is evaluated**; the other is never run. - **Both branches are mandatory.** There is no ternary without the `: else` part; for a one-sided condition use a modifier (`x if y`). ## Where it sits in the precedence table Ruby's precedence table lists `?` `:` below `&&`, `||` and the range operators `..`/`...`, and above modifier-`rescue`, the assignment operators, `defined?`, `not`, `and`/`or` and the statement modifiers. In practice that means: | Code | Parsed as | Note | |---|---|---| | `fee = resident ? 0 : 25` | `fee = (resident ? 0 : 25)` | assignment binds last, so no parentheses needed | | `ok = a \|\| b ? 1 : 2` | `ok = ((a \|\| b) ? 1 : 2)` | the `\|\|` is the test, which surprises readers | | `puts valid ? "yes" : "no"` | `puts((valid ? "yes" : "no"))` | the ternary is the argument | The second row is the classic readability trap: when the test contains operators, parenthesise it so the reader does not have to recall the table. RuboCop's `Style/TernaryParentheses` governs this; its default style in 1.91 is `require_no_parentheses`, and the config notes that `require_parentheses_when_complex` is expected to become the default in the next major release. ## Lexical gotchas: question marks everywhere Ruby method names may end in `?`, and `?a` is a **character literal** (a one-character string, `"a"`). So: - `permit.expired? ? "renew" : "ok"` is fine: the first `?` belongs to the method name, the second is the ternary. - Always put **spaces around `?` and `:`**; squeezing them invites misreadings, because `?a` can be a character literal and `:b` a symbol. ## When the ternary is a good fit Use it when: 1. the condition is **short** and reads as a yes/no question; 2. both results are **short values** (literals, variables, a simple call); 3. the result is being **assigned, returned or passed** — the ternary exists to produce a value. ```ruby badge = permit.resident? ? "R" : "V" fee = permit.expired? ? 0 : permit.daily_rate ``` ## When it is not - **Nested ternaries.** `a ? (b ? 1 : 2) : 3` forces the reader to match `?` and `:` pairs. RuboCop's `Style/NestedTernaryOperator` (enabled) flags it and its autocorrection turns the outer ternary into `if`/`else`. - **Multi-line ternaries.** Spreading `?` and `:` over lines is just an `if` in disguise; `Style/MultilineTernaryOperator` (enabled) says to use `if`/`unless` instead. - **Side effects in branches.** `paid ? charge! : notify!` works, but the ternary signals "choose a value"; an `if` states the intent of doing one of two things. - **Boolean results.** `permit.expired? ? true : false` is redundant: return the predicate call itself. RuboCop's `Style/IfWithBooleanLiteralBranches` (pending, so off until new cops are enabled) catches the `if` and ternary versions of this when the test is a comparison or a predicate method. - **Three or more outcomes.** Use `if`/`elsif`/`else` or `case`. The inverse rule also exists: `Style/OneLineConditional` (enabled) flags `if c then a else b end` written on a single line and prefers the ternary there. The two cops together push short two-way choices into the ternary and everything larger out of it. ## Summary The ternary is Ruby's compact, value-returning two-way conditional. It always has both branches, evaluates only one, and binds looser than `||` but tighter than `=`. Keep the test and both values short, parenthesise a compound test, never nest it, and never spread it across lines.

  • How does `ok = a || b ? 1 : 2` parse in Ruby?
    As `ok = ((a || b) ? 1 : 2)`. The ternary binds looser than `||` and tighter than `=`, so the whole `a || b` becomes the test and the chosen number is assigned. Parenthesising the test makes that explicit for readers who do not have the precedence table memorised.
  • Can a Ruby ternary omit the else part?
    No. The ternary syntax is always `test ? then_value : else_value`. For a one-sided condition use the modifier `value if test`, which evaluates to `nil` when the test is falsy, or a block `if` without `else`.

saying these in an interview costs you the question

  • A Ruby ternary evaluates both branches and then picks one of the results.
  • fee = resident ? 0 : 25 needs parentheses or it assigns resident to fee.
  • Nesting ternaries is fine as long as everything fits on one line.
  • You can write cond ? value without the colon part for a one-sided choice.
  • valid ? true : false is the idiomatic way to return a boolean from a predicate.