skip to content

In Ruby, what does reading an instance variable that was never assigned return, and how can that hide a typo?

level: juniorimportance: should knowfreq 52%

answer

  1. no error, no exception
  2. unlike an undefined local
  3. silent even under ruby -w since 3.0
  4. failure surfaces later on nil
  5. reader methods fail loudly on typos

basics

~20 s

An instance variable that was never assigned reads as nil, with no exception and, since Ruby 3.0, no warning even under -w. A misspelled @balnce therefore yields nil and fails later, far from the typo.

solid answer

~40 s

Instance variables spring into existence when first assigned; reading one that never was simply returns `nil`. There is no `NameError`, unlike an undefined local variable, and since Ruby 3.0 there is not even a verbose-mode warning. So a typo such as `@balnce + amount` in a `BankAccount` method evaluates `nil + amount` and raises `NoMethodError` ("undefined method '+' for nil") on that line or, worse, much later if the `nil` is stored first. Defences: set every instance variable in `initialize`, read state through methods so a misspelling raises `NoMethodError` naming the typo, and use `defined?(@balance)`, which returns `nil` when unset and `"instance-variable"` when set, if you must tell "never assigned" from "assigned nil".

code

ruby · 22 lines
ruby
class BankAccount
  def initialize(owner)
    @owner = owner
    @balance = 0
  end

  def deposit(amount)
    @balance = @balnce + amount   # typo: @balnce reads nil
  end
end

BankAccount.new("Ana").deposit(50)
# NoMethodError: undefined method '+' for nil

class Probe
  def check
    before = defined?(@x)
    @x = nil
    [before, defined?(@x)]
  end
end
Probe.new.check   # => [nil, "instance-variable"]

go deeper

for a junior

Recall that an unassigned instance variable reads nil with no error, and that a misspelled one therefore fails somewhere later on nil.

for a middle

Explain the contrast with locals and class variables, the Ruby 3.0 removal of the verbose warning, and defined?(@x) returning nil versus "instance-variable".

for a senior

Show how you keep this from reaching production: all state assigned in initialize, readers used inside the class, static checks that catch undeclared instance variables, and tests that run every method.

for a principal

Weigh how much a team should lean on conventions and static analysis to compensate for a runtime that never reports a misspelled instance variable, and where that effort pays off.

## Instance variables appear on first assignment A Ruby object has no declared fields. An **instance variable** such as `@balance` comes into being the first time some method running on that object assigns it. Before that moment the object simply does not have it, and reading it is still legal: the expression `@balance` evaluates to `nil`. ```ruby class BankAccount def initialize(owner) @owner = owner end def balance @balance # never assigned => nil end end BankAccount.new("Ana").balance # => nil ``` Nothing is raised and, on Ruby 3.0 and later, nothing is printed either. Earlier releases emitted `warning: instance variable @balance not initialized` when run with `-w`; Ruby 3.0 removed that warning, so there is no switch left that reports the read. ## How a typo hides Compare how Ruby treats three kinds of misspelled name: | Misspelling | What Ruby does | |---|---| | a local variable or method name, `balnce` | raises `NameError` (`undefined local variable or method`) at that line | | a method call with a receiver, `self.balnce` | raises `NoMethodError` naming `balnce` | | an instance variable, `@balnce` | returns `nil` silently | The third row is the dangerous one. Suppose `deposit` contains `@balance = @balnce + amount`: 1. `@balnce` reads `nil`. 2. `nil + amount` raises `NoMethodError` with `undefined method '+' for nil`. 3. The message names `+` and `nil`, not the misspelled variable, so the reader must work out which operand was `nil` and why. It gets worse when the `nil` is stored instead of used: `@limit = @limt` succeeds, and the failure appears in another method, or in another request, that finally calls something on `@limit`. ## Defences that work - **Assign every instance variable in `initialize`**, even to `nil`, so the full list of the object's state is written in one place and a reviewer can spot a name that appears nowhere else. - **Read state through methods** inside the class when a value is used in more than one place. A misspelled method name raises `NoMethodError` or `NameError` that names the typo at the line where it happens. - **Let tools look for it.** A static type checker that requires instance variables to be declared reports a misspelled one; the runtime itself never will. - **Tests that exercise each method** catch the `nil` early, because the failure usually shows up the first time the method runs. ## Unset versus explicitly nil Sometimes `nil` is a legitimate value, for example a `@closed_at` timestamp that stays `nil` while an account is open. Code that needs to know whether the variable was ever assigned can use the `defined?` keyword: - `defined?(@closed_at)` returns `nil` when the variable was never assigned. - It returns the string `"instance-variable"` once it has been assigned, even if the assigned value is `nil`. This distinction matters for memoization of values that can legitimately be `nil` or `false`: `@x ||= compute` recomputes every time the stored value is falsy, while `return @x if defined?(@x)` caches any result. ## What this is not - It is not a class-level default. Every object starts with no instance variables; `nil` is what a read of a missing one produces, not a stored value. - It is not the same for class variables: reading an `@@name` that was never assigned raises `NameError`. - It does not create the variable. After reading a missing `@balance`, the object still has no `@balance`; only assignment adds it. ## Interview angle A good answer names the behaviour, the consequence and the habit that neutralises it: - **Behaviour:** an unassigned instance variable reads `nil`, silently, on every current Ruby. - **Consequence:** a misspelling becomes a `nil` that fails somewhere else, with an error message about `nil` rather than about the name you got wrong. - **Habit:** assign all state in `initialize` and read it through methods where it matters, so the state of the object is listed in one place and typos fail loudly.

  • In Ruby, why does @total ||= expensive_sum recompute every time when the sum can be nil or false?
    `@total ||= expr` means `@total || @total = expr`. When the stored value is `nil` or `false` the left side is falsy, so the expression runs again. For values that can be falsy, memoize with `return @total if defined?(@total)` followed by `@total = expensive_sum`, because `defined?` reports assignment rather than truthiness.
  • In Ruby, does reading a never-assigned class variable behave like a missing instance variable?
    No. Reading an `@@name` that was never assigned raises `NameError` (`uninitialized class variable`), so class variables fail loudly where instance variables quietly return `nil`.

saying these in an interview costs you the question

  • Reading an unassigned @ivar raises NameError like an undefined local
  • Ruby -w still warns about uninitialized instance variables on 4.0
  • Reading @balance once creates it with the value nil
  • @x.nil? tells you whether @x was ever assigned
  • An unset @@var reads nil just like an unset @var