skip to content

If, Unless & Modifiers

In Ruby if and unless are expressions that return a value, and each can trail a statement as a modifier. Interviewers use them to see whether you write idiomatic, readable branches.

on this pageshow

explore

questions

6

In Ruby, what value does an if or unless expression return, including when no branch runs, and how do you use it?

level: juniorimportance: must knowfreq 62%

answer

  1. everything is an expression
  2. last evaluated expression wins
  3. assign the whole if block
  4. missing else branch
  5. nothing ran means nil

basics

~20 s

Ruby's if and unless are expressions: they return the value of the last expression evaluated in the branch that ran, and nil when no branch runs, so the whole conditional can be assigned to a variable or returned from a method.

solid answer

~40 s

In Ruby, `if`/`elsif`/`else` and `unless` are expressions, not bare statements. The whole construct evaluates to the last expression of whichever branch ran, so you can assign it directly — `fee = if permit.resident? ... else ... end` — or leave the conditional as a method's last expression and let it become the return value. When no branch runs — an `if` without `else` whose tests are all falsy, or an `unless` whose condition is truthy — the result is `nil`. The same holds for the modifier forms: `(25 if expired)` is `nil` when `expired` is falsy. That `nil` is the usual surprise: a method that ends in an `if` without an `else` returns `nil`, not `false`, when nothing matches.

code

ruby · 14 lines
ruby
permit_zone = "B"

fee = if permit_zone == "A"
  40
elsif permit_zone == "C"
  30
end
p fee                                  # => nil (no branch ran)

label = ("resident" unless permit_zone == "B")
p label                                # => nil (unless skipped)

logged = if permit_zone == "B" then puts "zone B" end
p logged                               # => nil (puts returns nil)

go deeper

for a junior

Say plainly that if and unless return a value: the last expression of the branch that ran, or nil when none ran. Show the assignment form fee = if ... else ... end.

for a middle

Walk through the nil cases: a missing else, a skipped unless, an empty branch, and a branch ending in puts or a bang method that returned nil. Explain why a method ending in an if relies on implicit return.

for a senior

Point out where an implicit nil escapes a method and breaks callers, and argue for an explicit else whenever nil is not a legitimate answer. Keep side-effect branches out of value-producing conditionals.

for a principal

Frame expression-oriented conditionals as a team convention: when assigning a conditional is encouraged, when an else is mandatory, and how linters and review keep value-returning branches total.

## Conditionals are expressions in Ruby Many languages split code into **statements** (which do something) and **expressions** (which produce a value). Ruby's control keywords blur that line: `if`, `elsif`, `else` and `unless` form an **expression**, and the whole construct has a value you can store, pass along or return. The language reference in the Ruby source says it directly: all the control expressions return a value, and "the result value of an `if` expression is the last value executed in the expression"; the same sentence is repeated for `unless`. Two consequences follow: - **Only one branch is evaluated.** Tests run top to bottom; the first truthy `if`/`elsif` test wins and the remaining tests are never evaluated. - **The value is whatever that branch evaluated last.** A branch is just a sequence of expressions, so its final expression becomes the value of the whole conditional. ## What each form returns | Form | Situation | Value of the expression | |---|---|---| | `if c then a else b end` | `c` truthy | value of `a` | | `if c then a else b end` | `c` falsy | value of `b` | | `if c then a end` (no `else`) | `c` falsy | `nil` | | `if c1 ... elsif c2 ... end` | every test falsy | `nil` | | `unless c then a end` | `c` truthy | `nil` | | `a if c` / `a unless c` | modifier skipped | `nil` | | `if c; end` (empty branch) | `c` truthy | `nil` | The rule behind every row is the same: **no branch ran, or the branch that ran was empty, so there is no "last value" and Ruby supplies `nil`**. ## Using the value: assignment and implicit return Because the conditional is an expression, idiomatic Ruby assigns it once instead of repeating the variable in every branch: ```ruby def permit_fee(permit) if permit.resident? 0 elsif permit.zone == "A" 40 else 25 end end fee = permit_fee(permit) status = if permit.expires_at > Time.now "valid" else "expired" end ``` Here `permit_fee` has no `return` keyword: a Ruby method returns the value of its last expression, and that expression is the whole `if`. The `status` assignment shows the other idiom: the variable is written once, on the first line, and the conditional supplies its value. Benefits of this style: 1. The variable is named **once**, so no branch can forget to set it. 2. The reader sees at a glance that the conditional's job is to **produce a value**. 3. Adding an `else` makes it obvious that **every path yields something**. ## The nil when no branch runs The case that bites is the missing `else`. Consider: ```ruby def permit_fee(permit) 25 if permit.zone == "A" end permit_fee(zone_b_permit) # => nil ``` The caller receives `nil`, and `nil + 10` raises `NoMethodError`. The same happens with `unless`: `unless permit.expired? then "ok" end` evaluates to `nil` when the permit *is* expired. Nothing warns you; the expression simply has no branch to take its value from. A related trap: the value is the **last expression evaluated**, not "the interesting one". A branch that ends with a logging call ends with that call's return value — `puts` returns `nil`, and an assignment returns the assigned value. So - `result = if ok then puts "ok" end` stores `nil` even though the branch ran; - `code = if ok then @last = 200 end` stores `200`, the value of the assignment, not the instance variable itself. ## When to lean on the value - **Do** use the value for small, total mappings: every branch produces a value of the same kind, and there is an `else`. - **Do** end a method with a conditional when the method's whole job is to choose a value. - **Add an explicit `else`** whenever `nil` would be a wrong answer for the caller, even if it looks redundant. - **Don't** assign a conditional whose branches are mostly side effects (logging, saving); the resulting value is incidental and invites misuse. - For many branches keyed on one value, `case` is usually clearer than a long `elsif` chain — it returns a value the same way. The mental model to carry into an interview: in Ruby a conditional is a value-producing expression; the value comes from the last expression of the branch that ran, and when no branch runs, the value is `nil`.

  • What does an if evaluate to when the branch that ran ends with a call such as puts?
    It evaluates to that call's return value, because the rule is always the last expression evaluated. `puts` returns `nil`, so a branch ending in `puts` makes the whole `if` nil even though it ran, and a branch ending in an assignment yields the assigned value. Put the value you want as the branch's final expression, after any logging.
  • Why assign the conditional once instead of assigning the variable inside every branch?
    Writing `fee = if ... else ... end` names the variable once, so no branch can forget to assign it, and it signals that the conditional exists to produce a value. Assigning inside each branch lets one branch silently skip the assignment; the variable then keeps its old value, or `nil` if it had none, and the bug surfaces far away.

saying these in an interview costs you the question

  • An if without an else returns false when its condition is false.
  • if is a statement in Ruby, so its result cannot be assigned to a variable.
  • An unless whose body is skipped returns the value of its condition.
  • An elsif chain evaluates every test and keeps the last matching branch.
  • A method needs an explicit return to hand back the value of a conditional.
open as a page

In Ruby, how do the x if y and x unless y statement modifiers work, and when should you prefer them over a full if block?

level: juniorimportance: must knowfreq 58%

basics

~20 s

A modifier puts the condition after the statement: Ruby evaluates the condition first and runs the statement only if it is truthy (if) or falsy (unless). Use it for a one-line body with a short condition, especially guard clauses.

open as a page

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%

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.

open as a page

In Ruby, when is unless the clearer choice, and why do style guides and RuboCop's Style/UnlessElse reject unless with an else branch?

level: juniorimportance: should knowfreq 48%

basics

~20 s

unless runs its body when the condition is falsy, so it reads well for one simple negative test. Adding else makes the reader negate twice to see when each branch runs, so the convention is to rewrite it as if with the positive case first.

open as a page

A Ruby parking-permit predicate ends in an if with no else, and an API now returns null instead of false — why, and how would you fix it?

level: seniorimportance: should knowfreq 30%

basics

~20 s

A Ruby method returns its last expression, and an if without else evaluates to nil when its test fails. nil passes truthiness checks but serialises as null and fails == false, so return a real boolean via a guard clause or an explicit else.

open as a page

In Ruby, why does p a if a = lookup raise NameError when a is new, while x = 1 if false leaves x as nil?

level: middleimportance: nice to knowfreq 22%

basics

~20 s

Ruby decides at parse time, in source order, whether a bare name is a local. In p a if a = lookup the body is parsed first, so a is a method call; x = 1 if false still declares x as nil.

open as a page