OOP Interview Prep
Inheritance

The `super` Keyword

Calling Up the Inheritance Chain

LinkedIn Hook

Every junior dev I've paired with has hit the same wall at least once.

They extend a class, write a constructor, and immediately get hit with a ReferenceError: Must call super constructor in derived class before accessing 'this'. The error message is clear. But why it happens — and what JavaScript is actually doing — is less obvious.

super is one of those keywords that looks simple until you try to explain it in an interview. Then you realise there are at least four things it can do, each with different rules.

Lesson 4.5 covers all of them. Calling the parent constructor. Calling parent methods. Using super inside overridden methods. Static super. And the exact moment forgetting super() in a subclass constructor goes from a bad idea to a hard crash.

Read the full lesson -> [link]

#OOP #JavaScript #Inheritance #InterviewPrep


The super Keyword thumbnail


What You'll Learn

  • What super is and why JavaScript requires it in subclass constructors
  • How super() calls the parent constructor and passes arguments up the chain
  • How super.method() calls a parent method from inside an overridden method
  • What happens when you forget super() in a subclass constructor (ReferenceError, and why)
  • How super works with static methods
  • The exact rules that govern when and where super can be used

The Analogy — Checking In With the Manager Before Using the Office

Picture a new employee joining a company. Before they can set up their desk, use the equipment, or access the systems, they have to check in with the manager who sets up their account. The manager runs a setup process that creates the core workspace. The employee can then add their own customizations on top.

In JavaScript inheritance, the parent class is the manager. The subclass is the new employee. Before the subclass constructor can use this — create properties, call methods, reference anything — it must call super() to let the parent class run its setup first. Only then is the workspace ready.

Skip the check-in and you get a hard error. JavaScript will not let you touch this in a subclass constructor until super() has been called.


What Is super?

super is a keyword with two distinct uses. Inside a constructor, super() calls the parent class constructor. Inside a method, super.methodName() calls the parent class version of that method. These look similar but operate differently.

super is not a variable. You cannot assign it, pass it, or log it. It is a binding that the JavaScript engine resolves based on the class hierarchy at the point where the containing method was defined. This distinction matters when you get into method overriding and static dispatch.


Example 1 — Calling the Parent Constructor with super()

The first and most common use. When a subclass defines a constructor, it must call super() before touching this.

class Vehicle {
  constructor(make, model, year) {
    // The parent constructor initializes the shared properties
    // Every vehicle has these, regardless of type
    this.make = make;
    this.model = model;
    this.year = year;
    this.isRunning = false;
  }

  start() {
    this.isRunning = true;
    return `${this.make} ${this.model} started.`;
  }

  stop() {
    this.isRunning = false;
    return `${this.make} ${this.model} stopped.`;
  }

  toString() {
    return `${this.year} ${this.make} ${this.model}`;
  }
}

class ElectricVehicle extends Vehicle {
  constructor(make, model, year, batteryCapacityKwh) {
    // super() MUST come before any reference to 'this'
    // It calls Vehicle's constructor with the shared arguments
    // 'this' does not exist until super() returns
    super(make, model, year);

    // Now 'this' is safe to use — parent setup is done
    // We add the EV-specific property on top
    this.batteryCapacityKwh = batteryCapacityKwh;
    this.chargeLevel = 100; // percent
  }

  charge(amount) {
    this.chargeLevel = Math.min(100, this.chargeLevel + amount);
    return `${this.toString()} charged to ${this.chargeLevel}%.`;
  }

  toString() {
    // super.toString() calls the parent version of toString()
    // We get the base string and add EV-specific info
    return `${super.toString()} [EV, ${this.batteryCapacityKwh}kWh]`;
  }
}

const tesla = new ElectricVehicle('Tesla', 'Model 3', 2024, 82);

console.log(tesla.make);               // "Tesla"
console.log(tesla.model);              // "Model 3"
console.log(tesla.year);               // 2024
console.log(tesla.batteryCapacityKwh); // 82
console.log(tesla.chargeLevel);        // 100

console.log(tesla.start());            // "Tesla Model 3 started."
console.log(tesla.toString());         // "2024 Tesla Model 3 [EV, 82kWh]"
console.log(tesla.charge(0));          // "2024 Tesla Model 3 [EV, 82kWh] charged to 100%."

Two things to notice. First: super(make, model, year) passes arguments directly to Vehicle's constructor. The parent gets exactly what it needs. Second: super.toString() inside ElectricVehicle.toString() calls Vehicle.toString(), which returns the base string. The subclass builds on it instead of duplicating it.

[PERSONAL EXPERIENCE]: The pattern of calling super.toString() and extending the result is something I use constantly when building class hierarchies for debug tooling. It keeps string representations composable — each layer of the hierarchy adds its own info without rewriting what the parent already computed.


Example 2 — super.method() in Method Overriding

You override a method to change its behavior. You call super.method() when you want the new behavior to include — rather than replace — the parent's behavior.

class Logger {
  constructor(name) {
    this.name = name;
    this.logs = [];
  }

  log(message) {
    // Base logger: store the message and print with timestamp
    const entry = `[${new Date().toISOString()}] [${this.name}] ${message}`;
    this.logs.push(entry);
    console.log(entry);
    return entry;
  }

  getLogs() {
    return [...this.logs]; // Return a copy — do not expose the internal array
  }
}

class ErrorLogger extends Logger {
  constructor(name) {
    super(name);
    this.errorCount = 0;
  }

  log(message) {
    // Override: increment the error counter, then delegate to the parent
    // super.log() handles timestamp formatting and storage — no duplication
    this.errorCount++;
    const prefix = `[ERROR #${this.errorCount}]`;
    return super.log(`${prefix} ${message}`);
    // Result: "[timestamp] [name] [ERROR #1] message"
  }

  summary() {
    return `${this.name}: ${this.errorCount} error(s) logged.`;
  }
}

class AuditLogger extends ErrorLogger {
  constructor(name, auditId) {
    super(name);
    this.auditId = auditId;
  }

  log(message) {
    // Multi-level override: adds audit ID, delegates to ErrorLogger
    // ErrorLogger.log() will then call Logger.log()
    // super.log() here refers to ErrorLogger.log(), not Logger.log()
    const auditTag = `[AUDIT:${this.auditId}]`;
    return super.log(`${auditTag} ${message}`);
    // Full result: "[timestamp] [name] [ERROR #1] [AUDIT:XYZ] message"
  }
}

const errLog = new ErrorLogger('PaymentService');
errLog.log('Card declined for user #4421');
// [2026-04-22T...] [PaymentService] [ERROR #1] Card declined for user #4421
errLog.log('Timeout connecting to gateway');
// [2026-04-22T...] [PaymentService] [ERROR #2] Timeout connecting to gateway
console.log(errLog.summary()); // "PaymentService: 2 error(s) logged."

const auditLog = new AuditLogger('OrderService', 'ORD-2026-04');
auditLog.log('Stock check failed');
// [2026-04-22T...] [OrderService] [ERROR #1] [AUDIT:ORD-2026-04] Stock check failed
console.log(auditLog.summary()); // "OrderService: 1 error(s) logged."
console.log(auditLog.getLogs().length); // 1

super.log() in AuditLogger resolves to ErrorLogger.log() — not Logger.log(). JavaScript looks up super relative to the class where the method is defined, not the class of the current object. This is called the "home object" binding, and it is fixed at method definition time. It does not change based on how the method is called.

The super Keyword visual 1


Example 3 — super with Static Methods

super works in static methods too. Inside a static method, super.staticMethod() calls the parent class's static method. The mechanics are the same — one level up the chain — but you are operating on the class itself, not on instances.

class MathUtils {
  static round(value, decimals = 0) {
    // Base static utility: standard rounding
    const factor = Math.pow(10, decimals);
    return Math.round(value * factor) / factor;
  }

  static describe() {
    // Static method on the base class — can be overridden
    return `MathUtils: general-purpose math helpers`;
  }
}

class FinanceMath extends MathUtils {
  static round(value, decimals = 2) {
    // Override: financial rounding always defaults to 2 decimal places
    // Delegate to the parent for the actual rounding logic
    return super.round(value, decimals);
    // super here refers to MathUtils (the parent class), not an instance
  }

  static roundUp(value, decimals = 2) {
    // Static method unique to FinanceMath — no parent version
    const factor = Math.pow(10, decimals);
    return Math.ceil(value * factor) / factor;
  }

  static describe() {
    // Call super.describe() to include the parent's description
    return `${super.describe()} | FinanceMath: finance-specific rounding`;
  }
}

console.log(MathUtils.round(3.14159));          // 3
console.log(MathUtils.round(3.14159, 3));       // 3.142

console.log(FinanceMath.round(3.14159));        // 3.14  (defaults to 2 decimals)
console.log(FinanceMath.round(3.14159, 4));     // 3.1416
console.log(FinanceMath.roundUp(3.141));        // 3.15

console.log(MathUtils.describe());
// "MathUtils: general-purpose math helpers"

console.log(FinanceMath.describe());
// "MathUtils: general-purpose math helpers | FinanceMath: finance-specific rounding"

Inside a static method, super refers to the parent class object — MathUtils — not an instance. Inside an instance method, super refers to the parent class's prototype. These are different lookup targets, but the syntax is identical. This symmetry is intentional: the super keyword behaves consistently in both contexts, just against different objects.

[UNIQUE INSIGHT]: Static super is less commonly tested in interviews, but it reveals whether a candidate understands that JavaScript classes are objects themselves and that extends creates a prototype link between the class objects as well as between their prototypes. FinanceMath.__proto__ === MathUtils is what makes static super work. Most candidates know instance super but can not explain why static super works at all.


Example 4 — The ReferenceError Trap: Forgetting super() in a Subclass Constructor

This is the most common super-related mistake. If you write a subclass constructor and access this before calling super(), JavaScript throws a ReferenceError. Not a TypeError. Not a silent failure. A hard crash with a specific message.

class Animal {
  constructor(name, sound) {
    this.name = name;
    this.sound = sound;
  }

  speak() {
    return `${this.name} says: ${this.sound}!`;
  }
}

// MISTAKE 1: Accessing 'this' before calling super()
class Dog extends Animal {
  constructor(name, breed) {
    // ERROR: Trying to use 'this' before super() is called
    // JavaScript will throw: ReferenceError: Must call super constructor
    // in derived class before accessing 'this' or returning from derived constructor
    this.breed = breed; // <-- crash happens here
    super(name, 'Woof');
  }
}

try {
  const dog = new Dog('Rex', 'Labrador');
} catch (e) {
  console.log(e instanceof ReferenceError); // true
  console.log(e.message);
  // "Must call super constructor in derived class before accessing
  //  'this' or returning from derived constructor"
}

// MISTAKE 2: Omitting super() entirely when a constructor is defined
class Cat extends Animal {
  constructor(name, indoor) {
    // No super() call at all
    // Same ReferenceError — 'this' is not initialized
    this.indoor = indoor; // <-- crash happens here
  }
}

try {
  const cat = new Cat('Whiskers', true);
} catch (e) {
  console.log(e instanceof ReferenceError); // true
  console.log(e.message);
  // Same error message as above
}

// CORRECT: super() first, then 'this'
class CorrectDog extends Animal {
  constructor(name, breed) {
    super(name, 'Woof'); // Parent runs first — 'this' is now initialized
    this.breed = breed;  // Safe to use 'this' now
  }

  describe() {
    // super.speak() calls Animal.speak() with the current instance's data
    return `${super.speak()} (Breed: ${this.breed})`;
  }
}

const rex = new CorrectDog('Rex', 'Labrador');
console.log(rex.name);       // "Rex"
console.log(rex.sound);      // "Woof"  (set by Animal constructor via super)
console.log(rex.breed);      // "Labrador"
console.log(rex.speak());    // "Rex says: Woof!"
console.log(rex.describe()); // "Rex says: Woof! (Breed: Labrador)"

Why does JavaScript enforce this order? Because the parent constructor is responsible for setting up the internal slot that backs this. In native classes, the object allocation itself happens inside the base class constructor. Until super() runs and returns, this is in an uninitialized state. The specification calls this "the [[thisBinding]] is not yet initialized". Accessing it before that initialization completes is a spec violation — hence ReferenceError, not TypeError.

[ORIGINAL DATA]: The exact error message differs slightly across engines. V8 (Node.js, Chrome) throws "Must call super constructor in derived class before accessing 'this' or returning from derived constructor". SpiderMonkey (Firefox) says "ReferenceError: |this| used uninitialized in ClassName class constructor". The type is always ReferenceError across all major engines — the message is engine-specific.

The super Keyword visual 2


Why You Can Omit super() When There Is No Explicit Constructor

A subclass with no constructor block at all works fine. JavaScript inserts a default constructor that looks like this:

constructor(...args) {
  super(...args);
}

The engine adds it automatically. You only hit the super() requirement when you write your own constructor. The moment you write constructor() { ... } in a subclass, the automatic version is replaced and you are responsible for calling super() manually.

class Bird extends Animal {
  // No constructor defined here
  // JavaScript inserts: constructor(...args) { super(...args); }
}

// This works fine — the implicit constructor calls super() for you
const parrot = new Bird('Polly', 'Squawk');
console.log(parrot.speak()); // "Polly says: Squawk!"

Common Mistakes

  • Accessing this before super() in a subclass constructor. The engine will throw a ReferenceError. this does not exist until super() returns. The fix is always the same: move super() to the first line of the constructor.

  • Calling super() in a base class constructor. super() is only valid in a subclass constructor. A base class that extends nothing cannot call super(). If you try, you get a SyntaxError during parsing — not at runtime.

  • Calling super() more than once in a constructor. Each constructor must call super() at most once. Calling it a second time throws a ReferenceError because this is already initialized and the spec does not permit re-initialization.

  • Assuming super.method() always resolves to the immediate parent. It resolves to the class where the containing method was defined, one level up. In a three-level chain, super.log() inside AuditLogger.log() calls ErrorLogger.log(), not Logger.log(). The resolution is fixed at method definition time — it does not re-resolve dynamically at call time.

  • Forgetting to pass arguments to super(). If the parent constructor requires arguments, super() with no arguments will leave those parent properties as undefined. The compiler does not warn you. You get silent property corruption that only shows up when you access the properties later.

  • Trying to use super in a plain function or object method. super is only valid inside class methods, class constructors, and object literal methods with a [[HomeObject]] binding. Using it in a standalone function throws a SyntaxError.


Interview Questions

Q: Why does JavaScript throw a ReferenceError when you access this before calling super() in a subclass constructor?

In JavaScript's class system, the object allocation (the creation of the object backing this) happens inside the base class constructor, not the subclass. Until super() runs and returns, this has no backing object — the spec calls this the uninitialized [[thisBinding]] state. Any attempt to read or write this before that initialization completes is a spec violation, so the engine throws a ReferenceError. The fix is always to move super() to the first line of the subclass constructor.

Q: What is the difference between super() and super.method()?

super() is a constructor call. It invokes the parent class's constructor function and is only valid inside a subclass constructor. super.method() is a method call. It invokes a specific method from the parent class's prototype and is valid inside any class method or accessor. Both use the same keyword but operate in completely different contexts. Using super() outside a constructor or super.method() inside a constructor in place of the other will produce a SyntaxError or incorrect behavior.

Q: How does super work in static methods, and what makes it possible?

In a static method, super refers to the parent class object itself (not its prototype). This works because extends creates a prototype link between the class objects: ChildClass.__proto__ === ParentClass. When the engine resolves super.staticMethod() inside a static method, it looks up the parent class via that prototype chain. Instance super and static super use the same keyword but point at different objects: instance methods look at ParentClass.prototype, static methods look at ParentClass directly.

Q: A candidate says "I can skip super() if I don't need any parent properties." Is that correct?

No. If a subclass defines an explicit constructor, it must call super() before accessing this, regardless of whether it needs parent properties. The requirement is not about inheriting data — it is about object initialization. The JavaScript engine requires the parent constructor to run to put the object into a valid state. Skipping super() and then touching this produces a ReferenceError. The only valid exception is returning a different object from the constructor entirely, which is a very rare and advanced pattern.

Q: If a subclass does not define a constructor, does it need to call super()?

No explicit call is needed because the subclass has no explicit constructor. JavaScript automatically inserts a default constructor equivalent to constructor(...args) { super(...args); } for any subclass that omits the constructor block. The super() call happens implicitly. The manual super() requirement only applies when the developer writes their own constructor body — that replaces the automatic version.


Quick Reference — Cheat Sheet

THE super KEYWORD — COMPLETE REFERENCE
========================================

USE 1: super() — Call Parent Constructor
------------------------------------------
class Child extends Parent {
  constructor(a, b, extra) {
    super(a, b);        // Must be first line before any 'this' access
    this.extra = extra; // Safe after super() returns
  }
}

Rules:
- Only valid inside a subclass constructor
- Must come before any reference to 'this'
- Must be called exactly once
- Pass required arguments to the parent constructor explicitly

USE 2: super.method() — Call Parent Method
--------------------------------------------
class Child extends Parent {
  greet() {
    const base = super.greet(); // Calls Parent.prototype.greet()
    return `${base} (extended)`;
  }
}

Rules:
- Valid inside any instance method or accessor
- Resolves relative to the class where the method is DEFINED (not the caller)
- Does NOT re-resolve dynamically — binding is fixed at definition time
- Can be called at any point inside the method — not restricted to first line

USE 3: super.staticMethod() — Call Parent Static Method
---------------------------------------------------------
class Child extends Parent {
  static util() {
    return super.util(); // Calls Parent.util() (the static method on the class object)
  }
}

Rules:
- Valid inside static methods only
- super refers to the parent CLASS object (not its prototype)
- Enabled by: ChildClass.__proto__ === ParentClass (set by extends)

THE ReferenceError TRAP
------------------------
class Child extends Parent {
  constructor(name) {
    this.name = name; // ERROR: ReferenceError before super()
    super();
  }
}
// ReferenceError: Must call super constructor in derived class
// before accessing 'this' or returning from derived constructor

Fix: move super() before all 'this' access.

OMITTING THE CONSTRUCTOR ENTIRELY
-----------------------------------
class Child extends Parent {
  // No constructor — JavaScript inserts automatically:
  // constructor(...args) { super(...args); }
}
// Safe — parent constructor is called with forwarded arguments

WHEN super IS NOT NEEDED / NOT ALLOWED
----------------------------------------
- Base classes (not extending anything): no super() call
- Plain functions: super is a SyntaxError
- Arrow functions: no own super binding (inherits from enclosing method)

QUICK DECISION TABLE
---------------------
Situation                              What to call
-----------                            ------------
Subclass constructor, need parent init  super(arg1, arg2, ...)
Override method, want parent behavior   super.methodName(args)
Override static, want parent behavior   super.staticName(args)    (inside static)
No constructor needed in subclass       omit constructor entirely

COMMON ERRORS
--------------
Error type        Cause
-----------       -----
ReferenceError    this used before super() in subclass constructor
ReferenceError    super() called twice in same constructor
SyntaxError       super() called in a base class (not extending anything)
SyntaxError       super used in a plain (non-class) function
Silent bug        super() called without required arguments -> parent props are undefined

Previous: Lesson 4.4 - Multiple Inheritance and the Diamond Problem Next: Lesson 4.6 - Composition vs Inheritance


This is Lesson 4.5 of the OOP Interview Prep Course — 8 chapters, 41 lessons.

On this page