skip to content

In a JavaScript class that uses `extends`, why must the derived constructor call `super()` before it reads or writes `this`, and what happens if it doesn't?

level: middleimportance: must knowfreq 80%

answer

  1. derived constructors allocate nothing
  2. the parent produces the object
  3. the binding starts uninitialized
  4. implicit return also reads this
  5. new.target decides the prototype

basics

~20 s

A derived constructor does not create its own this: the parent constructor allocates the instance, and super() is what runs it. Until super() returns, this is uninitialized, so reading or writing it throws a ReferenceError.

solid answer

~40 s

A class constructor is marked internally as either *base* or *derived*. A base constructor allocates the new object itself and binds `this` on entry. A derived constructor — one in a class written with `extends` — starts with `this` in an uninitialized state; `super(...)` invokes the parent constructor, which produces the object, and only then is `this` bound. So any access to `this` before `super()` throws a `ReferenceError`, and so does falling off the end of the constructor without ever calling `super()`, because the implicit return reads `this`. Calling `super()` twice also throws, since the binding is already initialized. That is also why `new.target` is threaded up the chain: the parent allocates the object but takes the prototype from the class you actually called with `new`.

code

javascript · 13 lines
javascript
class A {}
class B extends A {
  constructor() {
    try {
      this.x = 1;
    } catch (e) {
      console.log(e.constructor.name); // ReferenceError
    }
    super();
    this.x = 1; // fine now
  }
}
new B();

go deeper

for a junior

Remember the rule and state it plainly: in a class written with extends, call super() before using this, and pass the parent's constructor arguments through it.

for a middle

Explain the mechanism, not just the rule: the derived constructor's this starts uninitialized and super() is what binds it, which is why an early this access and a missing call both raise ReferenceError.

for a senior

Be ready to debug it from a stack trace — an early return or a guard clause above super() is the usual cause — and to explain why parent-allocates is what makes new.target forwarding and built-in subclassing work.

for a principal

Frame the design tradeoff: parent-allocates buys correct subclassing of exotic types at the cost of a constructor prologue you cannot customise, which shapes how you design base classes that need validated or injected state.

## Two kinds of class constructors When the engine creates a class constructor it records whether the class was written with `extends`. A class without `extends` gets a **base** constructor; a class with `extends` gets a **derived** constructor. That single flag changes who is responsible for producing the instance. A base constructor allocates the object itself. As soon as it is entered with `new`, the engine creates a fresh ordinary object whose prototype comes from `new.target.prototype`, and binds it as `this`. The body can therefore touch `this` on its very first line. A derived constructor allocates nothing. Its function environment record is created with `this` in an *uninitialized* state, and the only thing that can initialize it is the `super(...)` call. ## What `super()` actually performs `super(...)` is not a normal function call and it is not `Parent.call(this, ...)`. Roughly, it does three things: 1. Finds the parent constructor — the `[[Prototype]]` of the currently running constructor function, which `extends` set to `Parent`. 2. Constructs it, passing along `new.target` unchanged, so the object is allocated with the prototype of the class you originally wrote after `new`, not with `Parent.prototype`. 3. Binds the resulting object as `this` in the derived constructor's environment. ```js class Base {} class Derived extends Base { constructor() { super(); console.log(Object.getPrototypeOf(this) === Derived.prototype); // true } } new Derived(); ``` Step 2 is why subclassing works at all: `Base` created the object, but it used `new.target` (which is `Derived`) to pick the prototype. ## What throws, and why Reading or writing `this` while the binding is uninitialized is a `ReferenceError`. V8 phrases it as "Must call super constructor in derived class before accessing 'this' or returning from derived constructor". ```js class A {} class B extends A { constructor() { this.x = 1; // ReferenceError super(); } } ``` Three consequences follow from the same rule: - **Arguments to `super()` cannot mention `this`.** `super(this.name)` throws for the same reason — the argument list is evaluated before the parent runs. - **Returning without calling `super()` throws.** A constructor that ends normally implicitly returns `this`, which reads the binding. Only an explicit `return someObject;` escapes it, because an object return replaces `this` entirely. - **Calling `super()` twice throws.** The second call finds the binding already initialized and raises `ReferenceError`; it does not silently re-run the parent. Conversely, `super()` may appear conditionally, or several times in mutually exclusive branches, as long as exactly one call executes on any path. The check is dynamic, not syntactic. ```js class C extends A { constructor(flag) { if (flag) super(1); else super(2); } } ``` ## The implicit constructor If a derived class declares no constructor at all, the engine supplies one equivalent to `constructor(...args) { super(...args); }`. A base class with no constructor gets `constructor() {}`. This is why removing an explicit constructor from a subclass sometimes *fixes* an argument-forwarding bug: the default one forwards everything, while a hand-written `constructor() { super(); }` quietly drops arguments. ## Why the language is designed this way Other designs let the child allocate the object and then hand it to the parent for initialization. ECMAScript chose parent-allocates so that a subclass can extend types whose instances are not ordinary objects — the ones the engine must create with special internal structure. Because the parent constructor produces the object, subclassing built-ins yields a genuinely correct instance instead of a plain object with borrowed methods. The price is exactly the rule under discussion: the child has nothing to work with until the parent has returned. ## Practical guidance Put `super(...)` on the first line unless you have a specific reason not to; if you need to massage constructor arguments first, compute them into local variables (which are fine before `super()`), then call it. If you see a `ReferenceError` mentioning the super constructor in a stack trace, look for a `this` access on a path that skipped the call — commonly an early `return` or a guard clause placed above `super()`.

  • What does a derived constructor return if it never calls super() but does explicitly return an object?
    It returns that object, and no error is thrown. The implicit return is what reads `this` and triggers the `ReferenceError`; an explicit object return replaces the instance entirely and never touches the uninitialized binding. Returning a primitive from a derived constructor is different — the primitive is ignored and the engine falls back to `this`, which then throws if `super()` was skipped.
  • What is new.target inside the parent constructor when you call new Child(), and why does it matter?
    It is `Child`, not `Parent`. `super()` forwards `new.target` unchanged, so when the parent allocates the object it reads `new.target.prototype` and gets `Child.prototype`. Without that forwarding every subclass instance would be created with the parent's prototype and would lose its own methods.
  • Can super() appear somewhere other than the first statement of a derived constructor?
    Yes. The requirement is dynamic, not positional: any statement that does not read `this` may run first, including local variable computation, argument validation and logging. You may even call `super()` inside a branch, provided exactly one call executes on every path — two executions throw, zero executions throw at the implicit return.

saying these in an interview costs you the question

  • Claims super() is just Parent.call(this, args)
  • Says this exists before super() but is empty
  • Thinks skipping super() silently gives a plain object
  • Believes calling super() twice re-runs the parent
  • Assumes an omitted constructor forwards nothing to the parent

context