JavaScript Class Inheritance: extends, super and Overriding
JavaScript extends links two prototype chains, for instances and for statics. Why super() must come first, how overriding dispatches, and custom errors.
- Course: JavaScript study plan
- Module: Prototypes and classes
- Kind: Lesson
- Reading time: 13 min
- Runtime: Node 16
How do extends and super work in JavaScript?
In JavaScript, class Sub extends Base sets up two prototype links: Sub.prototype inherits from Base.prototype, so instances find parent methods, and Sub inherits from Base, so static members are found too. super() calls the parent constructor, which creates this, so it must run before this is used; super.method() calls the parent's version of a method.
Lesson
extends links two prototype chains: the subclass's prototype inherits from the parent's prototype (so instances find parent methods), and the subclass constructor inherits from the parent constructor (so statics are found too). super is the keyword for reaching the parent from either place. The rules are few but strict — call super() before touching this, fields initialise after it, super.method() uses the home object not the receiver — and the lesson closes with the two things people actually do with inheritance in JavaScript: extend Error, Map and Array, and prefer composition for everything else.
The two chains extends creates
class Shape {
constructor(name) { this.name = name; }
area() { return 0; }
describe() { return `${this.name} with area ${this.area().toFixed(2)}`; }
static create(...args) { return new this(...args); }
}
class Circle extends Shape {
constructor(r) {
super("circle"); // must run before any use of this
this.r = r;
}
area() { return Math.PI * this.r ** 2; } // override
describe() { return super.describe() + ` (r=${this.r})`; } // extend the parent's version
}
const c = new Circle(1);
Object.getPrototypeOf(Circle.prototype) === Shape.prototype; // instance chain: c → Circle.prototype → Shape.prototype → Object.prototype
Object.getPrototypeOf(Circle) === Shape; // static chain: Circle → Shape → Function.prototype
Circle.create(2) instanceof Circle; // true — static this is Circle
c instanceof Shape; // true
Overriding is just shadowing on the nearer prototype: c.area finds Circle.prototype.area first. describe, found on Shape.prototype, calls this.area() — and this is c, so it dispatches to the override. That late binding is the entire point of polymorphism, and it needs no annotations.
super() and construction order
In a derived class the this object is created by the base constructor — super() is the call that creates it — so:
- A derived constructor must call
super()before reading or writingthis(ReferenceError: Must call super constructor in derived class before accessing 'this'). - A derived class with no constructor gets
constructor(...args) { super(...args); }for free. - The derived class's fields initialise right after
super()returns, before the rest of the constructor body. So if a base constructor calls an overridden method, that method runs before the subclass's fields exist:
class Base { constructor() { this.init(); } init() {} }
class Derived extends Base {
value = 42;
init() { console.log(this.value); } // logs undefined — field not yet initialised
}
new Derived();
The fix is a design rule, not a trick: base constructors do not call overridable methods.
super.method() and the home object
super.describe() looks up describe on the prototype of the class where the method was written (the home object), not on this's prototype. That is what makes two levels of overriding work — each super call climbs exactly one level from where it was written, regardless of how deep the receiver's class is. It also means super in a method copied to another object still refers to the original parent. Static methods can use super.staticMethod() too, climbing the static chain.
Extending built-ins
class HttpError extends Error {
constructor(status, message) {
super(message); // sets message and captures the stack
this.name = "HttpError"; // otherwise the stack says "Error"
this.status = status;
}
}
try { throw new HttpError(404, "no such page"); }
catch (e) { e instanceof HttpError && e instanceof Error; } // true
class DefaultMap extends Map {
constructor(makeDefault) { super(); this.makeDefault = makeDefault; }
get(key) { if (!this.has(key)) this.set(key, this.makeDefault()); return super.get(key); }
}
class Stack extends Array {
peek() { return this[this.length - 1]; }
}
const s = Stack.from([1, 2, 3]);
s.map((x) => x * 2) instanceof Stack; // true — array methods build the subclass via Symbol.species
Array.isArray(s); // true
Subclassing Error is the one inheritance every codebase needs (module 7 goes deep). Map/Set subclasses work cleanly. Array subclasses work but carry the Symbol.species machinery (methods return the subclass) and were impossible before 2015 — prefer a class that holds an array unless you truly want array behaviour. Promise subclasses are legal and almost never worth it.
Mixins: multiple inheritance by function
JavaScript has single inheritance; a mixin is a function from class to class:
const Serializable = (Base) => class extends Base {
toJSON() { return { ...this, kind: this.constructor.name }; }
};
const Comparable = (Base) => class extends Base {
compareTo(other) { return this.valueOf() - other.valueOf(); }
};
class Money extends Serializable(Comparable(Object)) { /* … */ }
Each application inserts one prototype into the chain. Use sparingly — three mixins deep and nobody knows where a method comes from.
Composition over inheritance
A class hierarchy is right when subclasses are genuinely kinds of the parent, share most behaviour, and the hierarchy is shallow (one or two levels). It is wrong when you reach for it to share a utility method, when the "is-a" is a stretch (Button extends Rectangle), or when the tree keeps growing sideways. The alternative is holding an instance of the thing and delegating (this.logger.log(...)), or passing behaviour in as functions. Most production JavaScript has few deep hierarchies and many small classes that contain other objects.
Common mistakes
- Using
thisbeforesuper(); forgettingsuper(...args)when you add a constructor. - Base constructors calling methods the subclass overrides (fields are not ready).
- Forgetting
this.name = "…"in anErrorsubclass, so logs sayError. - Overriding a method without calling
super.method()when the parent's version did required work. - Deep hierarchies for code reuse;
extends Arraywhen an array field would do.
Interview angle
- "What does
extendsset up?" Two prototype links:Sub.prototype → Base.prototypefor instances andSub → Basefor statics. - "Why must
super()be called first?" The base constructor createsthis; before it there is no object. - "How does method overriding dispatch?" Property lookup finds the nearest prototype's version;
thisis the receiver, so parent code calls child overrides. - "How do you make a custom error?"
class X extends Error,super(message), setname, add fields. - "Inheritance versus composition?" Extend for real is-a relationships, shallow; otherwise hold and delegate.
Key takeaways
extendslinks instance and static chains;instanceoffollows the instance chain.super()first in derived constructors; fields initialise after it; base constructors should not call overridables.super.method()climbs one level from where the method was written (home object).- Extend
Error(setname),Map/Setfreely;Arrayworks viaSymbol.speciesbut usually hold an array instead. - Mixins are functions from class to class; prefer composition for sharing behaviour.
Common questions
Why must super() be called before this in a derived class?
In a derived class the base constructor creates the this object, and super() is the call that runs it. Before super() returns there is no object, so touching this throws ReferenceError: Must call super constructor in derived class before accessing 'this'.
When are subclass fields initialised in JavaScript?
Right after super() returns, before the rest of the derived constructor body. So if a base constructor calls a method the subclass overrides, that method runs before the subclass's fields exist and reads undefined — base constructors should not call overridable methods.
How do you create a custom error class in JavaScript?
Write class HttpError extends Error, call super(message) to set the message and capture the stack, set this.name = "HttpError" so logs name it correctly, and add fields such as status. Instances then pass both instanceof HttpError and instanceof Error.
Does JavaScript support multiple inheritance?
No, a class has exactly one parent. Mixins give a similar effect: a mixin is a function that takes a class and returns class extends Base { … }, and each application inserts one more prototype into the chain. Use them sparingly, because deep stacks hide where a method comes from.
Should I use inheritance or composition in JavaScript?
Inherit only for genuine is-a relationships that share most behaviour and stay shallow, one or two levels. To share a utility or model a loose relationship, compose instead: hold an instance of the other object and delegate to it, or pass behaviour in as functions.
Exercises
A shape hierarchy
Given Shape (with describe() printing <name>: area=<2dp> perimeter=<2dp>), write Circle(r), Rect(w, h) and Square(s) extends Rect. Circle.describe appends r=<r>, Rect.describe appends <w>x<h> — both by calling super.describe(). A square is a rect named square built with super(s, s, "square"). Read one shape per line (circle r, rect w h, square s), print each describe(), then total area=<2dp> shapes=<n> rects=<count instanceof Rect> squares=<count instanceof Square>, then chain=<constructor names along a Square instance's prototype chain>.
Example: circle 1, rect 2 3, square 4 →
circle: area=3.14 perimeter=6.28 r=1
rect: area=6.00 perimeter=10.00 2x3
square: area=16.00 perimeter=16.00 4x4
total area=25.14 shapes=3 rects=2 squares=1
chain=Square -> Rect -> Shape -> ObjectExtending Map, Array and Error
Write three built-in subclasses. DefaultMap extends Map takes a makeDefault function and its get creates the default for a missing key. Stack extends Array adds peek(). HttpError extends Error takes (status, message), sets name to "HttpError" and stores status. Input: line 1 words, line 2 numbers, line 3 a status code and a message. Print counts: <word=count sorted by word> instanceofMap=<> using the DefaultMap, then stack: peek=<> length=<> isArray=<Array.isArray> mapKeepsClass=<stack.map(...) instanceof Stack> doubledPeek=<peek of the doubled stack>, then throw the error and print <name> <status>: <message> instanceofError=<> stackStartsWithName=<e.stack.startsWith("HttpError")>.
Example: to be or not to be / 3 1 4 1 5 / 404 page not found →
counts: be=2 not=1 or=1 to=2 instanceofMap=true
stack: peek=5 length=5 isArray=true mapKeepsClass=true doubledPeek=10
HttpError 404: page not found instanceofError=true stackStartsWithName=trueIn this module: Prototypes and classes
- The prototype chain — how JavaScript objects inherit
- Constructor functions and what `new` really does
- Class syntax — fields, methods, statics, private and accessors
- Inheritance — extends, super, overriding and extending built-ins (this lesson)
- Property descriptors and the object protocols — toString, valueOf, toJSON, Symbol.toPrimitive
- Checkpoint — Prototypes and classes
← Class syntax — fields, methods, statics, private and accessors · Property descriptors and the object protocols — toString, valueOf, toJSON, Symbol.toPrimitive →