skip to content

Inside a JavaScript class declared with `extends`, what values may its constructor return, and how do those rules differ from a class that extends nothing?

level: middleimportance: should knowfreq 32%

answer

  1. base and derived are not treated alike
  2. one of them rejects stray returns
  3. null is not undefined here
  4. an object return can stand in for the instance
  5. two different error types are possible

basics

~20 s

A derived constructor may return only an object or undefined; returning any other value, including null, throws a TypeError. A base constructor is looser - it ignores every primitive return, including null, and simply hands back its instance.

solid answer

~40 s

A base class constructor follows the ordinary function rule: an object return replaces the instance, and every primitive return - `42`, a string, `null`, `undefined` - is silently ignored. A **derived** constructor tightens that. An object return still wins and becomes the instance, and `undefined` (or falling off the end) is fine, but any other value throws `TypeError: Derived constructors may only return object or undefined`. `null` is on the failing side here even though a base class ignores it. There is a second twist: returning an object from a derived constructor is legal *without* calling `super()`, because the returned object becomes the instance and the engine never needs to consult the uninitialised `this`. Returning nothing without `super()` does throw - a ReferenceError, because there is no `this` to hand back.

code

javascript · 20 lines
javascript
class Base {
  constructor() { this.tag = 'base'; return 42; }
}
console.log(new Base().tag); // 'base' - primitive ignored

class ReturnsObject extends Base {
  constructor() { return { tag: 'replaced' }; }
}
console.log(new ReturnsObject().tag);                    // 'replaced'
console.log(new ReturnsObject() instanceof ReturnsObject); // false

class ReturnsPrimitive extends Base {
  constructor() { super(); return 42; }
}
try { new ReturnsPrimitive(); } catch (e) { console.log(e.constructor.name); } // TypeError

class ReturnsNothing extends Base {
  constructor() { /* nothing */ }
}
try { new ReturnsNothing(); } catch (e) { console.log(e.constructor.name); } // ReferenceError

go deeper

for a junior

Know that class constructors normally return the instance and that you rarely write an explicit return at all. Recall that a class written with extends is stricter about what its constructor may return.

for a middle

State both rules precisely and explain the asymmetry: a base constructor ignores any primitive return, while a derived one accepts only an object or undefined and throws a TypeError otherwise, with null on the failing side.

for a senior

Distinguish the two failure modes by their cause - a TypeError for an illegal return value versus a ReferenceError for reading an uninitialised this when super() was skipped - and explain the legitimate override use, such as returning a Proxy wrapping the instance.

for a principal

Weigh whether a constructor should ever hand back something other than the object it built. Returning a wrapper buys transparent interception at the cost of every reader's assumption about new; decide whether a documented factory or an explicit wrap call is the better public contract.

## Two rules, not one When `new` finishes running a constructor body, it inspects the returned value. The classification depends on whether the class is a **base** class (declared without `extends`, or a plain constructor function) or a **derived** class (declared with `extends`). For a base constructor: - return an **object** → that object becomes the result; - return **anything else**, or nothing → the built `this` becomes the result. For a derived constructor: - return an **object** → that object becomes the result; - return **`undefined`**, or nothing → the `this` established by `super()` becomes the result; - return **anything else** → `TypeError`. So the difference is narrow but sharp: a base class treats a stray primitive return as a harmless no-op, and a derived class treats it as an error. ```js class Base { constructor() { this.tag = 'base'; return 42; } } new Base().tag; // 'base' - the 42 is ignored class Sub extends Base { constructor() { super(); return 42; } } new Sub(); // TypeError: Derived constructors may only return object or undefined ``` ## `null` sits on the strict side `null` is not an object in the specification's type system, and it is not `undefined` either. In a base constructor it therefore lands in the ignored bucket; in a derived constructor it lands in the error bucket: ```js class A { constructor() { return null; } } // fine - returns the instance class B extends A { constructor() { super(); return null; } } new B(); // TypeError ``` This asymmetry is the sharpest single fact in the topic, and it is exactly what a "what does this print" puzzle will hinge on. ## Why derived classes are stricter In a derived class, `this` does not exist at the start of the body - it is created by the parent construction and bound when `super()` returns. When the body finishes, the engine has two possible answers: the object you returned, or the `this` binding. If you returned a primitive, there is no coherent third option: handing back the primitive would break the guarantee that `new` yields an object, and ignoring it would mean silently reading a `this` that may not have been initialised at all. The specification chooses to reject it outright. This is also why a derived constructor can legally skip `super()` **provided** it returns an object: ```js class Odd extends Base { constructor() { return { tag: 'replaced' }; } } new Odd().tag; // 'replaced' ``` No `super()` runs, no parent initialisation happens, and the returned plain object is the result. It is not an instance of `Odd` - its prototype is `Object.prototype`, so `instanceof Odd` is `false`, none of `Odd`'s prototype methods are reachable, and `Odd`'s class field initialisers never ran (they run when `this` is initialised, i.e. as part of `super()` returning). Drop the return, and the same constructor fails: ```js class Broken extends Base { constructor() { /* no super(), no return */ } } new Broken(); // ReferenceError: Must call super constructor ... before returning from derived constructor ``` Note which error appears: `ReferenceError`, because the engine tried to read an uninitialised `this` binding, versus the `TypeError` you get for a bad return value. Being able to attribute each error to its cause is what separates a memorised answer from an understood one. ## Where the override is genuinely used The object-return override in a class constructor has two real uses: - **Wrapping the instance.** A class constructor that builds `this`, then returns `new Proxy(this, handler)`, so every caller of `new Thing()` transparently gets the intercepted object. Because the proxy's target is the real instance, `instanceof` still works. - **Returning a cached or interned instance** for value-like objects. Both are legitimate but deserve a comment in the code, because a reader does not expect `new` to hand back something other than the object it just built. ## Interview framing Expect a four-way puzzle: base returning a primitive, derived returning a primitive, derived returning an object with no `super()`, and derived returning nothing with no `super()`. The complete answer gives the four outcomes - ignored, `TypeError`, the object, `ReferenceError` - and explains the asymmetry in terms of where `this` comes from in a derived class.

  • What error does a derived constructor produce for `return null`, and why is that different from a base class?
    A `TypeError` - engines word it as "Derived constructors may only return object or undefined". `null` is neither an object in the specification's type system nor `undefined`, so it fails the derived check. A base constructor applies the looser rule, where any non-object return is simply ignored and the built instance comes back, so `return null` there is a harmless no-op.
  • Can a derived constructor return an object without ever calling `super()`?
    Yes, and it is legal precisely because the returned object becomes the result, so the engine never needs the uninitialised `this`. But nothing else ran: the parent constructor did not execute, the class's own field initialisers did not run, and the returned object has whatever prototype it already had - typically `Object.prototype` - so `instanceof` for that class is `false`.
  • A class constructor ends with `return new Proxy(this, handler)`. What does `new` give the caller, and does `instanceof` still work?
    The caller gets the proxy, because an object return overrides the instance. `instanceof` still works: the proxy forwards the prototype lookup to its target, which is the real instance linked to the class's prototype - unless the handler traps `getPrototypeOf` and lies. This is the main legitimate reason to use the override in a class constructor.

saying these in an interview costs you the question

  • Says class constructors cannot return anything at all
  • Thinks a derived constructor ignores primitives like a base one does
  • Treats null as an object because typeof says so
  • Assumes skipping super() is always an error
  • Confuses the ReferenceError for missing super with a bad-return TypeError

context