Background: In JavaScript, standard errors are described via the Error class. In large TypeScript projects, it is important to use your own error hierarchy for precise handling of situations, but there are nuances with types, inheritance, and 'instanceof' behavior.
Problem: When inheriting from Error, there can be difficulties: prototype may be lost, 'instanceof' may not work correctly, types may be described incorrectly, and serialization often leads to loss of stack trace. Without explicitly defining properties, bugs may arise when handling errors.
Solution: Correctly define custom errors by extending the Error class. It is necessary to explicitly specify the name, manually restore the prototype (important for ES5 and when compiling to CommonJS), and type the error fields.
Code example:
class ValidationError extends Error { code: number; constructor(message: string, code: number = 400) { super(message); Object.setPrototypeOf(this, ValidationError.prototype); this.name = 'ValidationError'; this.code = code; } } function process(user: string) { if (!user) throw new ValidationError('User required', 401); }
Key features:
Why can't you skip the call to Object.setPrototypeOf(this, ...)?
If you don't call Object.setPrototypeOf(this, Class.prototype), 'instanceof' ValidationError will not work when compiled to ES5/CommonJS or Babel. This will cause catch blocks for ValidationError not to catch the error.
class CustomErr extends Error {} const err = new CustomErr('msg'); console.log(err instanceof CustomErr); // false without setPrototypeOf
Can the name field of a custom error be omitted?
If you do not set the this.name property, the error stack and logs will be incorrect, complicating the search for causes and the classification of errors.
Should errors be serializable, and if so — how?
Errors should be correctly serializable (for example, for logging or network transfer), otherwise JSON.stringify(new Error()) will not produce message and stack. You should override the toJSON method.
class SerializableError extends Error { toJSON() { return { name: this.name, message: this.message, stack: this.stack }; } }
In a project, we simply did class MyError extends Error without restoring the prototype. Errors were caught via if (err instanceof MyError), but this did not work, and the code silently skipped handling critical situations.
Pros:
Cons:
We implemented a correct CustomError, explicitly set name, code, and toJSON, covered the handling of different types of errors with tests. In logs and catch handlers, the structure of errors became clear, thereby reducing the time spent on bug searches.
Pros:
Cons: