Why do Error subclasses invoke Object.setPrototypeOf(this, ...) in the constructor?

241 Views Asked by At

I do not understand why Object.setPrototypeOf(this, DatabaseConnectionError.prototype); is needed in the source code below.

export class DatabaseConnectionError extends Error {
  reason = "Error connecting to database";

  constructor() {
    super("Error connecting to database");
    Object.setPrototypeOf(this, DatabaseConnectionError.prototype);
  }
}
2

There are 2 best solutions below

0
Oskar Grosser On

Instantiating a class results in the instance inheriting from the class (having the class's prototype in its prototype-chain) by default, so there should not be a need to set the prototype to the class's explicitely.


While the following is not the case, here is a similar scenario: Setting the prototype after construction of the class allows the instance to e.g. inherit methods from a different class instead.

class A {
  constructor() {
    this.a = "initialized";
  }
}

class B {
  b() { return "inherited"; }
}

class C extends A {
  constructor() {
    super();
    
    Object.setPrototypeOf(this, B.prototype);
  }
}

const c = new C(); // Clearly constructed from C ...

console.log("c.a:", c.a);
console.log("c.b:", c.b);
console.log("c instanceof C?", c instanceof C); // ... but undeterminable via prototype-chain.

Note: This does however obfuscate from which class the object is constructed from; see third log above.

Regardless, I'd argue you should prefer a sensible prototype-chain over whatever above is.

0
user3840170 On

This is a workaround for a flaw in the object model of JavaScript versions earlier than ECMAScript 6 (2015), which is inherited by transpilers targetting those versions.

Before ECMAScript 6, the class keyword was reserved and non-functional, and object constructors were regular functions that you invoked with the new keyword.

function Dog(name) {
  this.name = name;
}

Dog.prototype.bark = function () {
  console.log(this.name + ' says woof!');
};

var clifford = new Dog('Clifford');
console.log(clifford instanceof Dog);      // true
clifford.bark();                           // Clifford says woof!

Inheritance was accomplished in two steps:

  • the instance prototype of the superclass (.prototype of the superclass) was set as the prototype of the instance prototype of the subclass (the prototype of the .prototype of the subclass), so that instances of the subclass are considered instances of the superclass;
  • the subclass constructor invoked the superclass constructor, using .call or .apply, passing in the received this value, in the hopes that the superclass will modify the object it was passed as this and return it, and afterwards returning that same object.

function Animal(name, sound) {
  this.name = name;
  this.sound = sound;
  // return this;   // this is implicit when invoked with 'new'
}

Animal.prototype.cry = function () {
  console.log(this.name + ' makes a loud "' + this.sound + '" sound.');
};

function Dog(name) {
  return Animal.apply(this, [name, 'woof']);
}

Dog.prototype = Object.create(Animal.prototype);

Dog.prototype.greet = function () {
  console.log(this.name + ' licks your face.');
};

var clifford = new Dog('Clifford');
console.log(clifford instanceof Animal);   // true
console.log(clifford instanceof Dog);      // true

// 'Clifford makes a loud "woof" sound.'
clifford.cry();
// 'Clifford licks your face.'
clifford.greet();

This, however, does not work for Error.

function MyError(message) {
  return Error.apply(this, arguments);
}

MyError.prototype = Object.create(Error.prototype);

var err = new MyError("message");
console.log(err instanceof MyError);   // false
console.log(err instanceof Error);     // true

This is because the Error constructor always creates a new object with a prototype of Error.prototype, and ignores the passed-in this value entirely. This means the returned object will not have the correct prototype, and will therefore not be considered an instance of the class from which it was just constructed.

To compensate for that, one can fix up the prototype after invoking the superclass constructor:

function MyError(message) {
  var newTarget = this.constructor;
  var ðis = Error.apply(this, arguments);
  Object.setPrototypeOf(ðis, newTarget.prototype);
  return ðis;
}

MyError.prototype = Object.create(Error.prototype);

var err = new MyError("message");
console.log(err instanceof MyError);   // true
console.log(err instanceof Error);     // true

Now, when you transpile ECMAScript 6 classes into an earlier dialect of the language, it may end up in a form similar to the previous example. To compensate for the fact that the object is created with the wrong prototype, you have to transplant its prototype after construction.

This situation resulting from the use of Error.apply is possible to avoid in some cases if the transpiler decides to opportunistically use Reflect.construct instead to invoke the superclass constructor. But if the code ever runs in an actual pre-ES6 engine, the prototype has to be fixed up one way or another. Given that Object.setPrototypeOf is also an ECMAScript 6 feature, it in turn also has to be polyfilled using the (then-)non-standard property __proto__, but that at least does not require using new syntax.

For best results, the setPrototypeOf call should be placed immediately after the super call in the constructor, and have the form Object.setPrototypeOf(this, new.target.prototype);. The former is so that methods and accessors of the subclass can be used by the constructor immediately, and the latter is so that instances of classes in turn derived from your subclass will also have the correct prototype.

class BadGenericError extends Error {
  constructor(...args) {
    super(...args);
    // replace BadGenericError.prototype
    // with new.target.prototype to fix the bug 
    Object.setPrototypeOf(this, BadGenericError.prototype);
  }
}

class BadSpecificError extends BadGenericError {}

// this works OK…
console.log(new BadGenericError() instanceof BadGenericError);
console.log(new BadSpecificError() instanceof BadGenericError);
// …but this does not
console.log(new BadSpecificError() instanceof BadSpecificError);