Custom errors, extending Error
Real programs fail in specific ways. A network call times out, a database rejects a write, a lookup finds nothing. When each of those has its own error type, your catch blocks can tell them apart and react precisely instead of squinting at a generic message string.
That’s the payoff of custom error classes. A network layer might throw HttpError, a database layer DbError, a search routine NotFoundError. Each name tells you what went wrong at a glance, and each class can carry data that fits its situation.
Your errors should keep the basics every error has: a message, a name, and ideally a stack. On top of that they can add fields of their own. An HttpError, for example, might expose a statusCode set to 404, 403, or 500.
As a codebase grows, its errors tend to arrange themselves into a family tree. A very specific HttpTimeoutError sits under the broader HttpError, which in turn is a kind of Error. That structure is what makes instanceof checks so useful, as you’ll see.
Extending Error
Take a function readProduct(json) whose job is to read JSON describing a product.
A valid input looks like this:
let json = `{ "sku": "A-100", "price": 49 }`;
Inside, we’ll lean on JSON.parse. Hand it malformed text and it throws a built-in SyntaxError. But syntactically correct JSON isn’t the same as valid product data. The string could parse cleanly and still be missing the sku or price fields our products must have.
So readProduct does two jobs: parse the JSON, then validate the result. A missing required field or a wrong format is an error — but not a SyntaxError, since the text parsed fine. It’s a different kind of problem. We’ll call it ValidationError and give it a class. That error should also record which field was at fault.
ValidationError will inherit from the built-in Error. To see what we’re building on, here’s roughly what Error does — this is pseudocode standing in for engine-level behavior, not source you’d write:
// Approximation of the built-in Error class, as the engine defines it
class Error {
constructor(message) {
this.message = message;
this.name = "Error"; // built-in subclasses use their own names
this.stack = <call stack>; // non-standard, but supported almost everywhere
}
}
Two things to notice. The constructor stashes the message you pass, and it hard-codes name to "Error". That second detail matters in a moment.
Now inherit ValidationError from it and take it for a spin:
class ValidationError extends Error {
constructor(message) {
super(message); // (1)
this.name = "ValidationError"; // (2)
}
}
function test() {
throw new ValidationError("Whoops!");
}
try {
test();
} catch(err) {
alert(err.message); // Whoops!
alert(err.name); // ValidationError
alert(err.stack); // a list of nested calls with line numbers for each
}
Line (1) calls the parent constructor. In a subclass constructor, calling super is mandatory — the engine won’t let you touch this until you have — and here it’s what sets message for you.
The catch is that same parent constructor also sets name to "Error". So line (2) overwrites it with "ValidationError". Without that line, err.name would read "Error" and your logs would lie about what happened.
name: “Error” ← wrong
name: “ValidationError” ✓
Now put it to work in readProduct(json):
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
// Usage
function readProduct(json) {
let product = JSON.parse(json);
if (!product.price) {
throw new ValidationError("No field: price");
}
if (!product.sku) {
throw new ValidationError("No field: sku");
}
return product;
}
// Working example with try..catch
try {
let product = readProduct('{ "price": 49 }');
} catch (err) {
if (err instanceof ValidationError) {
alert("Invalid data: " + err.message); // Invalid data: No field: sku
} else if (err instanceof SyntaxError) { // (*)
alert("JSON Syntax Error: " + err.message);
} else {
throw err; // unknown error, rethrow it (**)
}
}
One try..catch now covers two failure modes: our own ValidationError and the SyntaxError that JSON.parse throws on bad input. The catch block sorts them out by type.
Look at line (*): instanceof is doing the work of asking “what kind of error is this?” Line (**) matters just as much — if the error is neither kind we recognize, we rethrow it. A catch that swallows everything hides real bugs (a typo, a null reference) that you’d want to blow up loudly. Only handle what you understand; let the rest propagate.
Feed the reader below three kinds of input and watch a single catch route each to the right branch. The presets cover a valid product, a missing field (our ValidationError), and malformed text (the engine’s SyntaxError):
You could check the name string instead:
// ...
// instead of (err instanceof SyntaxError)
} else if (err.name == "SyntaxError") { // (*)
// ...
instanceof is the better tool here. Soon we’ll grow ValidationError into subtypes like PropertyRequiredError, and err instanceof ValidationError will still match every one of them, because subclasses are instances of their parent. A string comparison against err.name only matches one exact name, so it silently misses subclasses.
Further inheritance
ValidationError is broad. Lots of things count as invalid data: a field is missing, or it’s present but the wrong shape (a string price where a number belongs). Let’s carve out a sharper class, PropertyRequiredError, dedicated to the “missing field” case. It’ll also record which property went missing.
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
class PropertyRequiredError extends ValidationError {
constructor(property) {
super("No property: " + property);
this.name = "PropertyRequiredError";
this.property = property;
}
}
// Usage
function readProduct(json) {
let product = JSON.parse(json);
if (!product.price) {
throw new PropertyRequiredError("price");
}
if (!product.sku) {
throw new PropertyRequiredError("sku");
}
return product;
}
// Working example with try..catch
try {
let product = readProduct('{ "price": 49 }');
} catch (err) {
if (err instanceof ValidationError) {
alert("Invalid data: " + err.message); // Invalid data: No property: sku
alert(err.name); // PropertyRequiredError
alert(err.property); // sku
} else if (err instanceof SyntaxError) {
alert("JSON Syntax Error: " + err.message);
} else {
throw err; // unknown error, rethrow it
}
}
The new class is pleasant to use: pass the property name and nothing else. The constructor builds the readable message ("No property: sku") for you, so callers don’t repeat that string everywhere.
Notice the hierarchy at work in the catch. The thrown object is a PropertyRequiredError, yet err instanceof ValidationError is still true, so the first branch catches it. Inside that branch you can read err.property to learn exactly what was missing. One broad check, plus detail on demand.
Assigning name automatically
PropertyRequiredError sets this.name by hand, and so did ValidationError. Repeating this.name = <class name> in every custom error gets old fast, and it’s easy to typo the string so it no longer matches the class.
There’s a tidy fix. Make a base class that reads the name off the constructor itself: this.name = this.constructor.name. Every subclass inherits that line, so the name always tracks the actual class with zero repetition. Call the base MyError:
class MyError extends Error {
constructor(message) {
super(message);
this.name = this.constructor.name;
}
}
class ValidationError extends MyError { }
class PropertyRequiredError extends ValidationError {
constructor(property) {
super("No property: " + property);
this.property = property;
}
}
// name is correct
alert( new PropertyRequiredError("field").name ); // PropertyRequiredError
Why does this work? When you do new PropertyRequiredError(...), this.constructor points at PropertyRequiredError, and every named class exposes its name via Constructor.name. So this.constructor.name evaluates to "PropertyRequiredError" even though the assignment lives up in MyError. ValidationError now needs no constructor at all — it inherits MyError’s, which already does the right thing.
The result: much shorter error classes. ValidationError shrank to a single line, and no class repeats a name string.
See it for yourself. The base MyError holds the only this.name assignment, yet each subclass reports its own name — and one object counts as an instance of every class above it:
Wrapping exceptions
readProduct exists “to read the product data.” Along the way, different things can break. Today it’s SyntaxError and ValidationError; tomorrow the function might grow and throw new kinds too.
Whoever calls readProduct has to deal with all of that. Right now the caller stacks up if branches — check this class, check that class, rethrow the rest:
try {
...
readProduct() // the potential error source
...
} catch (err) {
if (err instanceof ValidationError) {
// handle validation errors
} else if (err instanceof SyntaxError) {
// handle syntax errors
} else {
throw err; // unknown error, rethrow it
}
}
Two types here, but a fuller readProduct could throw five. Do we really want callers enumerating every one of them, every single time they call?
Usually the honest answer is no. From the caller’s altitude, the interesting fact is “reading the product failed.” Why it failed is often beside the point — the message already describes it. And when the caller does want the gory details, it should be able to reach for them without matching on a laundry list of low-level types.
The technique for this is called wrapping exceptions:
- Define a class
ReadErrorfor a generic “couldn’t read the data” failure. - Inside
readProduct, catch the low-level errors (ValidationError,SyntaxError) and throw aReadErrorin their place. - Store the original error on the
ReadErroras itscause, so nothing is lost.
The caller then checks for ReadError and stops. If it needs specifics, it inspects err.cause.
cause: → original error
Here’s the full definition of ReadError and how readProduct and the caller use it:
class ReadError extends Error {
constructor(message, cause) {
super(message);
this.cause = cause;
this.name = 'ReadError';
}
}
class ValidationError extends Error { /*...*/ }
class PropertyRequiredError extends ValidationError { /* ... */ }
function validateProduct(product) {
if (!product.price) {
throw new PropertyRequiredError("price");
}
if (!product.sku) {
throw new PropertyRequiredError("sku");
}
}
function readProduct(json) {
let product;
try {
product = JSON.parse(json);
} catch (err) {
if (err instanceof SyntaxError) {
throw new ReadError("Syntax Error", err);
} else {
throw err;
}
}
try {
validateProduct(product);
} catch (err) {
if (err instanceof ValidationError) {
throw new ReadError("Validation Error", err);
} else {
throw err;
}
}
}
try {
readProduct('{bad json}');
} catch (e) {
if (e instanceof ReadError) {
alert(e);
// Original error: SyntaxError: Unexpected token b in JSON at position 1
alert("Original error: " + e.cause);
} else {
throw e;
}
}
readProduct behaves just as sketched: it catches syntax and validation errors, replaces each with a ReadError that carries the original on cause, and rethrows anything unexpected untouched.
From the outside, the whole error surface collapses to one check: e instanceof ReadError. No enumeration of low-level types. Need the underlying cause? It’s right there on e.cause.
Try both failure paths below. Whether the JSON is malformed or merely missing a field, the caller sees exactly one type — ReadError — while the original low-level error is preserved on .cause:
The name fits — you take “low level” exceptions and wrap them in a ReadError that speaks at a higher level of abstraction. The pattern is common across object-oriented code, where each layer translates the failures of the layer below into terms that make sense for its own callers.
Summary
- You can extend
Error(and the other built-in error classes) like any other class. Just remember to callsuper(message)and to setname— either by hand, or automatically withthis.name = this.constructor.namein a shared base class. instanceofis the go-to way to recognize an error type, and it respects inheritance, so a check against a parent class also matches its subclasses. When you’re handed an error from a third-party library and can’t reach its class, fall back to comparing thenamestring.- Wrapping exceptions is a widely used technique: a function catches low-level failures and rethrows a single higher-level error in their place. The original is commonly preserved on a property like
err.cause, though keeping it is a choice, not a requirement.