skip to content

A JavaScript base class constructor calls a method that the subclass overrides, and that override reads a property the subclass declared as a class field. What happens, and why?

level: seniorimportance: should knowfreq 35%

answer

  1. only subclasses break
  2. dynamic dispatch reaches the override early
  3. fields land after super() returns
  4. half-built object
  5. a bare declaration is still a definition

basics

~20 s

The override runs before the subclass's fields exist, so the property reads as undefined and the code usually throws a TypeError. Subclass field initializers run only after super() returns, while the base constructor body has already finished by then.

solid answer

~40 s

The overridden method runs too early. Field initializers of a **base** class run right after the instance is created and before its constructor body; field initializers of a **derived** class run only when `super()` returns. So the sequence is: base fields, base constructor body — which calls the override — and only then the derived fields. Inside that override, `this.items` is `undefined`, and something like `this.items.push(x)` throws a TypeError. The same ordering causes a quieter bug: if the base constructor assigns `this.value = 42` and the subclass merely declares `value;`, the field definition runs afterwards and overwrites it with `undefined`. The fix is to stop calling overridable methods from a constructor — do the work in an explicit `init()` the caller invokes, or a static factory.

code

javascript · 16 lines
javascript
class Base {
  constructor() { this.setup(); }
  setup() {}
}

class Child extends Base {
  items = [];
  setup() { this.items.push('registered'); }
}

try {
  new Child();
} catch (e) {
  console.log(e.constructor.name, e.message);
  // TypeError Cannot read properties of undefined (reading 'push')
}

go deeper

for a junior

Remember the one-line order: base fields, base constructor, then subclass fields. That alone explains why a value declared in a subclass is missing while the parent constructor is still running.

for a middle

Walk the full sequence out loud, including that this is unavailable in a derived constructor until super() returns, and show that a bare declaration still defines the property as undefined.

for a senior

Diagnose it from the symptom — a TypeError that only appears for subclasses, only during construction — and give the durable fix of keeping overridable work out of constructors behind an explicit init step or factory.

for a principal

Own the convention across a codebase: constructors establish invariants and call nothing overridable, extension points are explicit lifecycle hooks, and inherited state is passed through super arguments rather than restated as subclass fields.

## The exact timeline For `new Child()` where `class Child extends Base`, the observable order is: 1. `Child`'s constructor begins; `this` does not exist yet. 2. `super()` runs `Base`'s constructor. 3. Inside `Base`, the instance is created and **`Base`'s field initializers run**, in source order. 4. `Base`'s constructor body runs. 5. `super()` returns, `this` becomes available in `Child` — and **`Child`'s field initializers run now**, in source order. 6. The rest of `Child`'s constructor body runs. Everything the base constructor does happens at step 4. Everything the subclass declared as a field happens at step 5. A base constructor that calls out to a method therefore reaches a **half-built** object whenever the subclass contributes state through fields. ## The crash ```js class Base { constructor() { this.setup(); } setup() {} } class Child extends Base { items = []; setup() { this.items.push('registered'); } } new Child(); // TypeError: Cannot read properties of undefined (reading 'push') ``` Dispatch is dynamic, so `this.setup()` in the base constructor finds `Child`'s override immediately — the prototype chain is fully wired long before any field initializer runs. But `items` has not been defined yet, so `this.items` is `undefined`. The error message points at `push`, several frames away from the real cause, which is why this one eats debugging time. ## The silent version ```js class Base { constructor() { this.value = 42; } } class Child extends Base { value; } new Child().value; // undefined ``` Nothing throws. A field declared with **no initializer is still a definition**: it defines the property as `undefined` at step 5, wiping the 42 the base assigned at step 4. This bites hardest when someone adds a bare declaration to a subclass purely for documentation and quietly breaks initialization inherited from the parent. A closely related case is a subclass field that *does* have an initializer and shares a name with something the base set — same outcome, the later definition wins. ## Why the ordering is what it is A derived instance does not exist until the base constructor has produced it. Field initializers may use `this`, so they cannot run before there is a `this` — which is exactly the moment `super()` returns. The ordering is not an oversight; it is the only point at which a derived field initializer could safely evaluate. ## Diagnosing it The signature is a failure that appears only for subclasses, only during construction, and vanishes if you move the same code out of the constructor. Two quick confirmations: - Log inside the override: `console.log(this.constructor.name, Object.keys(this))` — you will see the base's fields present and the subclass's absent. - Temporarily convert the subclass field to an assignment in its constructor body after `super()`. If the crash moves or disappears, the ordering is your cause. ## Fixes, in order of preference **Do not call overridable methods from a constructor.** This is the durable fix. Make construction do only the work the class itself owns, and move anything a subclass might specialise into an explicit step: ```js class Base { init() {} // called by whoever creates the object static create(...args) { const obj = new this(...args); obj.init(); return obj; } } ``` By the time `init()` runs, construction is completely finished and every field exists. **Lazy access.** If a base hook genuinely must run early, have it use a getter that materialises state on demand (`get items() { return this.#items ??= []; }`) rather than a field the subclass declares. **Assign instead of declare.** Where a subclass must keep the base's value, drop the field declaration and assign in the constructor body after `super()`, or do not restate the property at all. **Do not "fix" it by reordering the class body.** The order of declarations within one class body matters, but no reordering can make a derived field exist before the base constructor body runs — that boundary is fixed by the language. ## The one ordering rule worth memorising Base fields → base constructor body → derived fields → derived constructor body. Nearly every construction-time surprise in a class hierarchy falls out of that single line.

  • Would moving the field declaration to the bottom of the subclass body fix it?
    No. Ordering within one class body only decides the order of that class's own initializers relative to each other. Every derived field still runs after `super()` returns, which is after the entire base constructor body has finished. No arrangement of declarations can make a derived field exist while the base constructor is still executing.
  • How would you restructure a base class that genuinely needs subclass-provided state during setup?
    Take the work out of the constructor. Let the constructor establish only what the class itself owns, and expose an explicit `init()` — often invoked by a static factory that constructs then initialises — so subclass fields are all in place before any overridable hook runs. Alternatively, have the base read the value through a getter the subclass overrides, since a getter is evaluated on demand rather than at construction.
  • Can a field initializer in a base class read a value that the derived constructor will set later?
    No, and the reason is the same timeline: base field initializers run at step 3, long before the derived constructor body at step 6. A base initializer that reads `this.somethingTheChildSets` sees `undefined`. If a base field must depend on subclass configuration, pass the value through the constructor arguments to `super(...)` instead.

saying these in an interview costs you the question

  • Says the subclass field exists because fields hoist
  • Blames dynamic dispatch rather than initialization order
  • Claims reordering the class body fixes it
  • Thinks a field with no initializer does nothing
  • Suggests calling super() last to give fields a head start

context