In Ruby, what value does an if or unless expression return, including when no branch runs, and how do you use it?
answer
- everything is an expression
- last evaluated expression wins
- assign the whole if block
- missing else branch
- nothing ran means nil
basics
~20 sRuby'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 sIn 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 linespermit_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
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.
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.
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.
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.