super

Published by Programming Line in Ruby. Last updated .

When a subclass overrides a method, it often needs to build on what the parent class already does rather than replace it entirely. Ruby’s super keyword lets a method call the next matching version of itself further up the ancestor chain, so a subclass can add behavior without duplicating the parent’s logic. It shows up constantly in initialize methods, in modules mixed into a class, and anywhere you want composition instead of copy-paste.

Overview: How super Works

Every object in Ruby resolves a method call by walking its ancestor chain: first the object’s singleton class (if it has singleton methods), then its class, then any modules mixed into that class via include (searched in reverse order of inclusion), then the superclass and its own included modules, and so on up through Object, Kernel, and BasicObject. You can see this chain yourself by calling SomeClass.ancestors. When you define a method in a subclass with the same name as one further up the chain, you are not deleting the original method — it still exists, just shadowed. super is how you reach it.

Calling super inside a method resumes the search for that same method name starting right after the point in the ancestor chain where the current method was found, and calls whatever it finds there. This is exactly the mechanism that makes mixins and multi-level inheritance work together: a module included between a class and its superclass can itself call super and hand control further up the chain, forming a chain of cooperating methods rather than one method simply replacing another.

super comes in three forms that behave differently:

  • Bare super (no parentheses) automatically forwards the exact arguments, keyword arguments, and block that the current method received — not necessarily their original values, since if you reassign a parameter before calling super, the new value is what gets forwarded.
  • super() with empty parentheses calls the next method with no arguments at all, even if the current method received some.
  • super(args) calls the next method with exactly the arguments you list, which may differ from what the current method received.

One more detail worth knowing: include inserts a module below the including class in the ancestor chain (the class itself is searched first, then the module), while prepend inserts a module above the class (the module is searched first). A prepended module’s method runs before the class’s own method, and calling super from inside it reaches the class’s method — effectively wrapping it. This is less common than plain include, but it explains why what super reaches depends on how a module was mixed in, not just where the method text happens to be written.

Syntax

def method_name(args)
  super          # forwards this method's own arguments and block automatically
  super()        # calls the next method with no arguments
  super(a, b)    # calls the next method with exactly these arguments
end
Form Arguments passed When to use it
super Same args, keyword args, and block the current method received The common case — you want the parent to do what it always does with the same input
super() None You want to call the parent version but deliberately not pass along any arguments
super(args) Exactly what you list You want to transform or filter the arguments before the parent sees them

Examples

Example 1: Bare super in a simple override

class Animal
  def initialize(name)
    @name = name
  end

  def speak
    "#{@name} makes a sound."
  end
end

class Dog < Animal
  def speak
    "#{super} Woof!"
  end
end

dog = Dog.new("Rex")
puts dog.speak

Output:

Rex makes a sound. Woof!

Dog doesn't override initialize, so Dog.new("Rex") runs Animal#initialize directly and sets @name. Inside Dog#speak, the bare super call takes no arguments — because speak itself received none — and calls Animal#speak, which returns "Rex makes a sound.". That string is interpolated into Dog#speak's own return value, producing "Rex makes a sound. Woof!".

Example 2: super with explicit arguments and super()

class Vehicle
  def initialize(make, model)
    @make = make
    @model = model
  end

  def describe
    "#{@make} #{@model}"
  end
end

class Car < Vehicle
  def initialize(make, model, doors:)
    super(make, model)
    @doors = doors
  end

  def describe
    "#{super()} with #{@doors} doors"
  end
end

car = Car.new("Toyota", "Corolla", doors: 4)
puts car.describe

Output:

Toyota Corolla with 4 doors

Car#initialize takes a keyword argument doors: that Vehicle#initialize knows nothing about, so it can't use bare super — it explicitly calls super(make, model) to forward only the two positional arguments Vehicle#initialize expects. In describe, super() with empty parentheses calls Vehicle#describe with no arguments (which is correct, since Vehicle#describe doesn't take any), and the result is woven into the subclass's own string.

Example 3: super traveling through a mixed-in module

module Loggable
  def describe
    "[LOG] #{super}"
  end
end

class Vehicle
  def describe
    "#{@make} #{@model}"
  end
end

class Car < Vehicle
  include Loggable

  def initialize(make, model)
    @make = make
    @model = model
  end

  def describe
    "#{super} (a car)"
  end
end

puts Car.ancestors.first(4).inspect
puts Car.new("Honda", "Civic").describe

Output:

[Car, Loggable, Vehicle, Object]
[LOG] Honda Civic (a car)

Because Loggable is included in Car, it sits between Car and Vehicle in the ancestor chain, as the printed ancestors list confirms. When Car#describe calls super, Ruby doesn't jump straight to Vehicle — it finds Loggable#describe first, which itself calls super to reach Vehicle#describe. Three cooperating methods contribute to one final string.

How It Works Step by Step

Walking through Example 3 shows exactly how super travels through both a mixed-in module and a superclass:

  1. Ruby calls describe on a Car instance. Method lookup starts at Car itself, the front of the ancestor chain, and finds Car#describe immediately.
  2. Car#describe runs and hits super. Ruby resumes the search for describe starting right after Car in the ancestor chain — that's Loggable, because include Loggable inserted it between Car and Vehicle.
  3. Loggable#describe runs and itself calls super. Ruby resumes the search after Loggable, which lands on Vehicle.
  4. Vehicle#describe runs, returns "Honda Civic", and has no further super call, so the chain starts unwinding from here.
  5. Back in Loggable#describe, the returned string is wrapped into "[LOG] Honda Civic".
  6. Back in Car#describe, that result is wrapped again into "[LOG] Honda Civic (a car)", which is what actually gets printed.

Common Mistakes

Mistake 1: Bare super forwards ALL current arguments, even ones the parent can't accept

It's easy to assume bare super is always safe. It isn't — it forwards every argument the current method received, so if a subclass method takes more parameters than the version it's overriding, bare super raises an ArgumentError.

class Shape
  def initialize(color)
    @color = color
  end
end

class Square < Shape
  def initialize(color, side)
    super  # BUG: bare super forwards BOTH color and side, but Shape#initialize takes only 1 argument
    @side = side
  end
end

begin
  Square.new("red", 4)
rescue ArgumentError => e
  puts "Error: #{e.message}"
end

Output:

Error: wrong number of arguments (given 2, expected 1)

The fix is to switch to explicit super(color), sending only the argument Shape#initialize actually expects:

class Shape
  def initialize(color)
    @color = color
  end
end

class Square < Shape
  def initialize(color, side)
    super(color)  # FIX: pass only the argument Shape#initialize expects
    @side = side
  end
end

square = Square.new("red", 4)
puts "Side: #{square.instance_variable_get(:@side)}"

Output:

Side: 4

Mistake 2: Calling super when nothing further up the chain defines that method

super only works if some ancestor actually defines a method with the same name. Calling it from a method that exists only in the current class raises NoMethodError.

class Base
  def greet
    "Hello from Base"
  end
end

class Sub < Base
  def farewell
    "Bye! #{super}"  # BUG: Base has no `farewell` method for super to call
  end
end

begin
  Sub.new.farewell
rescue NoMethodError
  puts "Error: no superclass method to call"
end

Output:

Error: no superclass method to call

Before adding super to a method, check that a class or module further up ancestors actually defines that same method name.

Mistake 3: Forgetting to call super in initialize leaves inherited state unset

Unlike some languages, Ruby never calls a parent's initialize for you automatically. If a subclass defines its own initialize and never calls super, the parent's setup code simply never runs, and any instance variables it would have set stay nil.

class Person
  attr_reader :name

  def initialize(name)
    @name = name
  end
end

class Employee < Person
  def initialize(name, salary)
    @salary = salary  # BUG: forgot to call super, so @name is never set
  end

  def info
    "#{name} earns #{@salary}"
  end
end

emp = Employee.new("Ava", 50_000)
puts emp.info

Output:

 earns 50000

name silently returns nil (which interpolates as an empty string) because Person#initialize never ran. Adding super(name) fixes it:

class Person
  attr_reader :name

  def initialize(name)
    @name = name
  end
end

class Employee < Person
  def initialize(name, salary)
    super(name)  # FIX: let Person#initialize set @name
    @salary = salary
  end

  def info
    "#{name} earns #{@salary}"
  end
end

emp = Employee.new("Ava", 50_000)
puts emp.info

Output:

Ava earns 50000

Best Practices

  • Prefer bare super to automatically forward arguments, keyword arguments, and blocks unless you deliberately need to change what's passed on.
  • Always call super in initialize when subclassing a class that sets up its own state, unless you intentionally want to skip that setup.
  • Use super() with empty parentheses when you want to call the parent version but deliberately drop the arguments the current method received — never rely on bare super just because it happens to work.
  • Remember that super follows the full ancestor chain, including mixed-in modules, not just the literal superclass — check SomeClass.ancestors when the flow isn't obvious.
  • Call super early in initialize (usually first) so inherited instance variables are set up before the rest of the method touches them.
  • Don't sprinkle super calls defensively wrapped in rescue NoMethodError — if you're not sure the method exists further up the chain, that's a sign to check ancestors, not to silence the error.

Practice Exercises

  1. Define a class Book with initialize(title, author) and a to_s method that returns "#{title} by #{author}". Then define EBook < Book that adds a file_size_mb keyword argument and overrides to_s to call super and append " (#{file_size_mb}MB)". Instantiate an EBook and print it.
  2. Write a module Timestamped with a save method that prints "Saved at a fixed point in time" and then calls super. Include it in a class Document that defines its own save method printing "Document saved". Check Document.ancestors, and predict what happens when you call Document.new.save if Document#save does not itself call super.
  3. Build a three-level hierarchy: Vehicle, Car < Vehicle, and SportsCar < Car. Give each class a describe method that calls super and prepends its own class name, producing something like "SportsCar -> Car -> Vehicle". Instantiate a SportsCar and call describe — predict the exact string before running it.

Summary

  • super calls the next matching method up an object's ancestor chain — the same chain SomeClass.ancestors shows you, which includes mixed-in modules, not just superclasses.
  • Bare super forwards the current method's own arguments, keyword arguments, and block automatically; super() forwards nothing; super(args) forwards exactly what you specify.
  • include inserts a module below the including class in the chain; prepend inserts it above, changing what super reaches from inside the class's own method.
  • Forgetting to call super in a subclass's initialize silently skips the parent's setup — instance variables the parent would have set stay nil.
  • Calling bare super with the wrong arity, or calling super when nothing further up the chain defines that method, raises ArgumentError or NoMethodError at runtime — the syntax itself is always valid either way.