129

I'm trying to throw a custom error with my "CustomError" class name printed in the console instead of "Error", with no success:

class CustomError extends Error { 
    constructor(message: string) {
      super(`Lorem "${message}" ipsum dolor.`);
      this.name = 'CustomError';
    }
}
throw new CustomError('foo'); 

The output is Uncaught Error: Lorem "foo" ipsum dolor.

What I expect: Uncaught CustomError: Lorem "foo" ipsum dolor.

I wonder if that can be done using TS only (without messing with JS prototypes)?

darksoulsong
  • 13,988
  • 14
  • 47
  • 90

8 Answers8

116

Are you using typescript version 2.1, and transpiling to ES5? Check this section of the breaking changes page for possible issues and workaround: https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work

The relevant bit:

As a recommendation, you can manually adjust the prototype immediately after any super(...) calls.

class FooError extends Error {
    constructor(m: string) {
        super(m);

        // Set the prototype explicitly.
        Object.setPrototypeOf(this, FooError.prototype);
    }

    sayHello() {
        return "hello " + this.message;
    }
}

However, any subclass of FooError will have to manually set the prototype as well. For runtimes that don't support Object.setPrototypeOf, you may instead be able to use __proto__.

Unfortunately, these workarounds will not work on Internet Explorer 10 and prior. One can manually copy methods from the prototype onto the instance itself (i.e. FooError.prototype onto this), but the prototype chain itself cannot be fixed.

Community
  • 1
  • 1
Vidar
  • 1,777
  • 1
  • 11
  • 15
  • 2
    Yes, I'm transpiling to ES5. I tried to set the prototype as you suggested, but unfortunately, it did not work. – darksoulsong Dec 12 '16 at 15:13
  • 4
    It is worth noting that unfortunately, `Object.setPrototypeOf(this, this.constructor.prototype)` will _not_ work here, the class has to be referenced explicitly. – John Weisz Jan 25 '17 at 15:41
  • 1
    @JohnWeisz if the fix you are mentioning worked, then there will be nothing to be fix in the first place, if you could read the prototype from `this.constructor` at this point, then the prototype chain would already be intact. – thetrompf Aug 17 '17 at 22:20
  • 6
    As of TypeScript 2.2 there seem to be a way to do it without hardcoding the exact type in the constructor via: `Object.setPrototypeOf(this, new.target.prototype);` https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#example – Grabofus Oct 16 '19 at 16:15
101

The problem is that Javascript's built-in class Error breaks the prototype chain by switching the object to be constructed (i.e. this) to a new, different object, when you call super and that new object doesn't have the expected prototype chain, i.e. it's an instance of Error not of CustomError.

This problem can be elegantly solved using 'new.target', which is supported since Typescript 2.2, see here: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html

class CustomError extends Error {
  constructor(message?: string) {
    // 'Error' breaks prototype chain here
    super(message); 

    // restore prototype chain   
    const actualProto = new.target.prototype;

    if (Object.setPrototypeOf) { Object.setPrototypeOf(this, actualProto); } 
    else { this.__proto__ = actualProto; } 
  }
}

Using new.target has the advantage that you don't have to hardcode the prototype, like some other answers here proposed. That again has the advantage that classes inheriting from CustomError will automatically also get the correct prototype chain.

If you were to hardcode the prototype (e.g. Object.setPrototype(this, CustomError.prototype)), CustomError itself would have a working prototype chain, but any classes inheriting from CustomError would be broken, e.g. instances of a class VeryCustomError < CustomError would not be instanceof VeryCustomError as expected, but only instanceof CustomError.

See also: https://github.com/Microsoft/TypeScript/issues/13965#issuecomment-278570200

Kristian Hanekamp
  • 2,429
  • 1
  • 21
  • 13
  • 1
    Should `this.__proto__` be private or public? – lonix Feb 02 '19 at 18:14
  • You can make it private I think, since you don't try to access it outside of the constructor. So as long as you only use it in TypeScript that gets compiled to ES5, it should be no problem, since this TypeScript feature is only a compile-time feature. The field will always be publicly accessible in your compiled code. Currently the actual "private class fields" proposal is only in experimental stage. And the `__proto__` property is also deprecated and only somewhat standardized to be somewhat backwards compatible. See [MDN](https://developer.mozilla.org) for more infos on both. – Trve Apr 17 '19 at 16:15
  • 1
    Or you can simply do something like `(this as any).__proto__ = actualProto;`. This is what I did. Hacky, but so I don't need to change any class declarations. – Trve Apr 17 '19 at 16:19
  • 2
    I would also add `get [Symbol.toStringTag]() { return 'CustomError'; }` `static get [Symbol.species]() { return CustomError; }` – grgrssll Jan 28 '21 at 05:17
  • 1
    Does this also set the name? – philk Feb 18 '22 at 10:15
  • 1
    @philk No, it doesn't. I still have to set it manually. – chehsunliu Apr 21 '23 at 03:43
30

It works correctly in ES2015 (https://jsfiddle.net/x40n2gyr/). Most likely, the problem is that the TypeScript compiler is transpiling to ES5, and Error cannot be correctly subclassed using only ES5 features; it can only be correctly subclassed using ES2015 and above features (class or, more obscurely, Reflect.construct). This is because when you call Error as a function (rather than via new or, in ES2015, super or Reflect.construct), it ignores this and creates a new Error.

You'll probably have to live with the imperfect output until you can target ES2015 or higher...

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
30

As of TypeScript 2.2 it can be done via new.target.prototype. https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#example

class CustomError extends Error {
    constructor(message?: string) {
        super(message); // 'Error' breaks prototype chain here
        this.name = 'CustomError';
        Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain
    }
}
Grabofus
  • 1,924
  • 14
  • 17
10

I literally never post on SO, but my team is working on a TypeScript project, and we needed to create many custom error classes, while also targeting es5. It would have been incredibly tedious to do the suggested fix in every single error class. But we found that we were able to have a downstream effect on all subsequent error classes by creating a main custom error class, and having the rest of our errors extend that class. Inside of that main error class we did the following to have that downstream effect of updating the prototype:

class MainErrorClass extends Error {
  constructor() {
    super()
    Object.setPrototypeOf(this, new.target.prototype)
  }
}

class SomeNewError extends MainErrorClass {} 

...

Using new.target.prototype was the key to getting all of the inheriting error classes to be updated without needing to update the constructor of each one.

Just hoping this saves someone else a headache in the future!

EmiPixi
  • 101
  • 1
  • 3
7

I ran into the same problem in my typescript project a few days ago. To make it work, I use the implementation from MDN using only vanilla js. So your error would look something like the following:

function CustomError(message) {
  this.name = 'CustomError';
  this.message = message || 'Default Message';
  this.stack = (new Error()).stack;
}
CustomError.prototype = Object.create(Error.prototype);
CustomError.prototype.constructor = CustomError;

throw new CustomError('foo');

It doesn't seem to work in SO code snippet, but it does in the chrome console and in my typescript project:

enter image description here

Mμ.
  • 8,382
  • 3
  • 26
  • 36
  • 1
    Liked the idea, failed to implement it in typescript – Julius Š. Jun 16 '17 at 12:34
  • Really? It worked on my typescript react native project. – Mμ. Jun 16 '17 at 12:35
  • Well, I also wanted to abstract the boilerplate part and probably failed in doing so, I'll try again without any other shinanigens. – Julius Š. Jun 19 '17 at 06:57
  • Nice idea, but it fails with this message when `strict: true` in tsconfig: https://stackoverflow.com/questions/43623461/new-expression-whose-target-lacks-a-construct-signature-in-typescript – André Jul 06 '17 at 05:14
  • It works, but breaks error formatting in the browser console. In the Chrome console your CustomError instances aren't displayed with the usual nicer formatting that "true" Errors get, but in a "generic object" format, which is harder to read. – Kristian Hanekamp Jan 19 '18 at 13:50
  • @Mμ. On which TypeScript version does it work for you? – Tomasz Waszczyk Sep 21 '18 at 07:23
  • @Mμ. does react native have this prototype chain problem in the first place even? – philk Feb 18 '22 at 10:21
3

I was having this problem in a nodejs server. what worked for me was to transpile down to es2017 in which these issues seems to be fixed.

Edit tsconfig to


    "target": "es2017"

user12575927
  • 311
  • 3
  • 8
1

Try this...

class CustomError extends Error { 

  constructor(message: string) {
    super(`Lorem "${message}" ipsum dolor.`)
  }

  get name() { return this.constructor.name }

}

throw new CustomError('foo')
flcoder
  • 713
  • 4
  • 14
  • But this does not seem to use the already existing functionality and requires us to re-implement the wheel so to speak. Or am i mistaken? – JoshuaTree Dec 10 '20 at 12:23
  • @JoshuaTree What already existing functionality does the solution not use? Which wheel is being re-implemented? You might not be mistaken but your arguments could be a little more specific. – flcoder Dec 11 '20 at 01:37
  • Sorry, my mistake. I checked the typescript src and I see it's an interface, not an actual object with getters and setters. Your implementation does not override any existing functionality. – JoshuaTree Dec 11 '20 at 11:38