Topics

Jump to a topic.

Search questions

Find a question by title, tag, or summary.

How do you extend built-ins like Array and Error?

Questions in this chapter (7)
  • Draft
  • hard
  • Askedrarely
  • Verified October 2026

Answer

Built-in constructors such as Array, Error, Map, and Promise allocate their own special objects (an exotic array, an object with [[ErrorData]], and so on). Because new passes new.target through super(...), the built-in uses GetPrototypeFromConstructor(newTarget, ...) to give that object the subclass's prototype. So class Stack extends Array produces a real array (Array.isArray is true, length works) that inherits from Stack.prototype. ES5-style Array.call(this) can't do this: it creates and discards an exotic object, and this stays ordinary.

Symbol.species. map, filter, slice, splice, concat, flat, and flatMap build their results with ArraySpeciesCreate: read this.constructor, then its [Symbol.species] (a getter that returns the constructor itself by default), and call new Species(length). So:

  • results are instances of your subclass, and your methods work on them;
  • your constructor is called with a length, so a constructor with a different signature silently misbehaves (the label below becomes 2);
  • static get [Symbol.species]() { return Array; } makes those methods return plain arrays. Promise.prototype.then, ArrayBuffer, RegExp and typed arrays consult species too. Map and Set define the getter, but their methods don't use it.

Error. The most common legitimate subclass. Call super(message, options) (so cause works), set name yourself (it defaults to "Error"), and add fields. instanceof works.

Recommendation: subclass Error; subclass Array/Map/Promise only when instances must pass Array.isArray/be accepted as the built-in and your constructor accepts the built-in's arguments. Otherwise wrap the built-in in a class and expose only the methods you need. MDN notes that the standards committee considers the built-in subclassing mechanism over-engineered, with performance and security costs.

Example

class Stack extends Array {
  peek() { return this.at(-1); }
}
const s = Stack.of(1, 2, 3);
console.log(s instanceof Stack, Array.isArray(s), s.peek()); // → true true 3
console.log(s.filter((n) => n > 1) instanceof Stack); // → true: species builds a Stack

class Names extends Array {
  constructor(label) { super(); this.label = label; }
}
const names = new Names('users');
names.push('ada', 'grace');
console.log(names.map((n) => n.toUpperCase()).label); // → 2: map called new Names(2)

class Safe extends Array {
  static get [Symbol.species]() { return Array; }
}
console.log(Safe.from([1, 2]).map((n) => n * 2) instanceof Safe); // → false

class HttpError extends Error {
  constructor(status, message, options) {
    super(message, options);
    this.name = 'HttpError';
    this.status = status;
  }
}
const err = new HttpError(404, 'not found', { cause: 'x' });
console.log(err instanceof Error, String(err), err.cause); // → true HttpError: not found x

Pitfalls

  • Changing the constructor signature of an Array/Promise subclass. Species calls new Subclass(length) or new Subclass(executor), so extra required parameters break internal calls.
  • Forgetting name. class E extends Error {} gives new E('m').name === 'Error', so logs and String(err) show Error unless you set it.
  • Overriding set/add on a Map/Set subclass without knowing the constructor calls it for every initial entry. The override runs during construction, possibly before your own fields exist.
  • Relying on structuredClone to keep the subclass. The copy of an error is a plain Error, not an instance of your class.

Follow-ups

Why can't ES5-style Array.call(this) subclass Array?
It can't forward new.target. The call allocates a fresh exotic array and discards it, so this stays an ordinary object and Array.isArray(this) is false.
What does Symbol.species on Array return?
this, via a getter on the constructor, so Stack[Symbol.species] is Stack unless you override it.
Do I need Object.setPrototypeOf(this, new.target.prototype) in a custom error?
Not with native classes: super(message) already creates the error from new.target.prototype. The workaround is for code compiled down to ES5, where Error.call(this) can't forward new.target.

Exercises

Predict the output

class Queue extends Array {
  static get [Symbol.species]() { return Array; }
}
const q = Queue.of(1, 2, 3);
console.log(q.slice(1) instanceof Queue, q.slice(1) instanceof Array);

class E1 extends Error {}
console.log(new E1('m').name);

class LoudMap extends Map {
  set(key, value) { console.log('set', key); return super.set(key, value); }
}
new LoudMap([['a', 1]]);