skip to content

Since Ruby 3.0, what RuntimeErrors do class variables raise when overtaken by an ancestor's definition or accessed from the top level?

level: middleimportance: nice to knowfreq 22%

answer

  1. two definitions, one name
  2. ancestor defined it later
  3. message says overtaken by
  4. no enclosing class at top level
  5. defined? still returns nil safely

basics

~20 s

Since Ruby 3.0, accessing a class variable that both a class and one of its ancestors define raises RuntimeError (overtaken by the ancestor), and any @@ access at top level raises RuntimeError: class variable access from toplevel.

solid answer

~40 s

Ruby 3.0 turned two class-variable situations into errors. **Overtaking**: if `Circle` defines `@@registry` and later `Shape` (its superclass) or an included module defines the same name, the next access from Circle raises `RuntimeError: class variable @@registry of Circle is overtaken by Shape`; before 3.0 this was only a warning in verbose mode. **Top level**: code outside any class or module has no class to own the variable, so `@@count = 0` or reading `@@count` there raises `RuntimeError: class variable access from toplevel`. `defined?(@@count)` at top level does not raise and returns `nil`. Both errors usually appear after reordering `require`s or moving code, and the fix is one definition in the right class.

code

ruby · 13 lines
ruby
module Registry
  @@items = []
end

class Shape
  @@items = [:shape]
  def self.items = @@items
end

Shape.items           # => [:shape]
Shape.include(Registry)
Shape.items
# RuntimeError: class variable @@items of Shape is overtaken by Registry

go deeper

for a junior

Remember that @@ variables only make sense inside a class or module and that top-level @@ access raises RuntimeError in modern Ruby.

for a middle

Explain how two definitions of one @@ name arise from load order, and read the overtaken-by message to tell which class or module holds the other one.

for a senior

Diagnose the error after a require-order or autoload change, fix it by keeping one owner or moving to class-level ivars, and grep for shared @@ names in mixins.

for a principal

Use the 3.0 errors as evidence in a codebase policy: order-dependent shared state is a design risk, so steer teams away from @@ rather than patching each conflict.

## Background: how @@ lookup works A **class variable** (`@@name`) is looked up through the class tree: Ruby checks the current class, then its ancestors, including included modules. Assignment follows the same search, and only if no class in the chain has the variable does it create one in the current class. That design means the **same name can end up defined in two places** if they are created in the "wrong" order. ## Error 1: overtaken by an ancestor ```ruby class Shape end class Circle < Shape @@registry = [:circle] def self.registry = @@registry end Circle.registry # => [:circle] class Shape @@registry = [:shape] end Circle.registry # RuntimeError: class variable @@registry of Circle is overtaken by Shape ``` Step by step: 1. When Circle's body ran, no ancestor had `@@registry`, so it was created **in Circle**. 2. When Shape was reopened, Shape had no `@@registry` of its own and nothing above it did either, so a **second** variable was created in Shape. 3. On the next read, Ruby finds the name in both Circle and Shape. Since Ruby 3.0 it refuses to guess and raises `RuntimeError`. The same error appears when a **module** that defines `@@registry` is included into a class that already has its own, with the message naming the module. Before Ruby 3.0 this situation produced only a warning, and only in verbose mode, so the conflict could sit unnoticed. Typical real-world causes: - a base class file loaded **after** a subclass file, for example after changing `require` order or autoload behaviour; - a shared module that grew a `@@` variable with a common name; - a monkey-patch that assigns `@@something` in a parent class. The fix is structural: define the variable **once**, in the class that should own it, before subclasses use it, or better, replace it with class-level instance variables so each class owns its own. ## Error 2: access from the top level ```ruby # at the top of a script, outside any class @@count = 0 # RuntimeError: class variable access from toplevel ``` Top-level code runs with no enclosing class or module definition, so there is no class that could own a class variable. Since Ruby 3.0, reading or writing `@@count` there raises `RuntimeError`. A few details: - It applies to **both** reads and writes. - `defined?(@@count)` at top level is allowed; it does not raise and returns `nil`. - Code inside a method or block that was **defined at top level** is still top-level code for this purpose. ## Finding and fixing an overtaking conflict The error message names both parties: the class you read from and the class or module that also defines the name. To confirm, ask each one for the class variables it defines itself; `Module#class_variables(false)` leaves out inherited ones: ```ruby Circle.class_variables(false) # => [:@@registry] Shape.class_variables(false) # => [:@@registry] ``` Two entries for one name is the conflict. Then: 1. Decide which class should own the value, normally the base class. 2. Make sure that class assigns it before any subclass or module touches it, or remove the assignment from the subclass. 3. Better still, replace the variable with class-level instance variables read through `def self.` methods, so no two classes can compete for one name. ## Summary | Situation | Ruby 3.0 and later (4.0 included) | |---|---| | Same `@@name` in a class and in an ancestor or included module | `RuntimeError`, "... is overtaken by ..." | | `@@name` read or written at top level | `RuntimeError`, "class variable access from toplevel" | | `defined?(@@name)` at top level | `nil`, no error | | Reading a `@@name` defined nowhere | `NameError`, uninitialized class variable | ## Why interviewers ask it It is a quick check of whether a candidate has migrated real code across the 3.0 boundary and understands why class variables are fragile: their meaning depends on **definition order** across files. A candidate who can explain the overtaking error has, in effect, explained why many Ruby teams avoid `@@` entirely.

  • Why does Ruby raise instead of simply using the nearest definition, as method lookup would?
    Because the two variables really are different storage, and which one code meant depends on file load order. Silently picking one hides a bug where half the code updates one slot and half the other. Since 3.0 Ruby surfaces the conflict so it is fixed at its source.
  • How do you check for a class variable at the top level of a script without raising?
    Use `defined?(@@name)`, which Ruby allows at top level and which returns `nil` there. Any real read or write of `@@name` at top level raises `RuntimeError`. For a specific class, `Shape.class_variable_defined?(:@@name)` answers without touching top-level scope at all.

saying these in an interview costs you the question

  • When a parent and a child both define @@x, the child's value silently wins.
  • A top-level @@count = 0 creates a class variable every class can read.
  • The overtaking conflict still only warns under ruby -w in Ruby 4.0.
  • defined?(@@x) at top level raises the same RuntimeError as reading it.