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?
answer
- base and derived are not treated alike
- one of them rejects stray returns
- null is not undefined here
- an object return can stand in for the instance
- two different error types are possible
basics
~20 sA 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 sA 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 linesclass 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); } // ReferenceErrorgo deeper
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.
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.
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.
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