137

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)?

0

8 Answers 8

127

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/94fb0618c7185f86afec26e6b4d2d6eb7c049e47/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.

Sign up to request clarification or add additional context in comments.

4 Comments

Yes, I'm transpiling to ES5. I tried to set the prototype as you suggested, but unfortunately, it did not work.
It is worth noting that unfortunately, Object.setPrototypeOf(this, this.constructor.prototype) will not work here, the class has to be referenced explicitly.
@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.
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); typescriptlang.org/docs/handbook/release-notes/…
104

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

6 Comments

Should this.__proto__ be private or public?
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.
I would also add get [Symbol.toStringTag]() { return 'CustomError'; } static get [Symbol.species]() { return CustomError; }
Does this also set the name?
@philk No, it doesn't. I still have to set it manually.
|
33

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
    }
}

3 Comments

This doesn't set the .name property
I believe using this.name = new.target.name works for sorting the name without having to hard code it.
Have to turn on --keep-names in esbuild to make new.target.name work.
31

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...

2 Comments

This. This. This and again this.
As mentioned in other answered here, there is already a work around so it's better to accept the additional complexity and implement the prototype chain update.
12

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!

2 Comments

Check this answer, it seems to handle more cases: stackoverflow.com/a/48342359/131120
I wonder if there is an eslint rule to notify you about the missing prototype correction?
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

7 Comments

Liked the idea, failed to implement it in typescript
Really? It worked on my typescript react native project.
Well, I also wanted to abstract the boilerplate part and probably failed in doing so, I'll try again without any other shinanigens.
Nice idea, but it fails with this message when strict: true in tsconfig: stackoverflow.com/questions/43623461/…
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.
|
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"

Comments

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')

3 Comments

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 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.
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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.