When we develop something, we often need our own error classes to reflect specific things that may go wrong in our tasks. For errors in network operations we may need HttpError
, for database operations DbError
, for searching operations NotFoundError
and so on.
Our errors should support basic error properties like message
, name
and, preferably, stack
. But they also may have other properties of their own, e.g. HttpError
objects may have statusCode
property with a value like 404
or 403
or 500
.
JavaScript allows to use throw
with any argument, so technically our custom error classes don’t need to inherit from Error
. But if we inherit, then it becomes possible to use obj instanceof Error
to identify error objects. So it’s better to inherit from it.
As we build our application, our own errors naturally form a hierarchy, for instance HttpTimeoutError
may inherit from HttpError
, and so on.
As an example, let’s consider a function readUser(json)
that should read JSON
with user data. Here’s an example of how a valid json may look:
let json = `{ "name": "John", "age": 30 }`;
Internally, we’ll use JSON.parse
. If it receives malformed json, then it throws SyntaxError
.
But even if json is syntactically correct, that doesn’t mean that it’s a valid user, right? It may miss the necessary data. For instance, if may not have name
and age
properties that are essential for our users.
Our function readUser(json)
will not only read JSON, but check (“validate”) the data. If there are no required fields, or the format is wrong, then that’s an error. And that’s not a SyntaxError
, because the data is syntactically correct, but another kind of error. We’ll call it ValidationError
and create a class for it. An error of that kind should also carry the information about the offending field.
Our ValidationError
class should inherit from the built-in Error
class.
That class is built-in, but we should have its approximate code before our eyes, to understand what we’re extending.
// The "pseudocode" for the built-in Error class defined by JavaScript itself class Error { constructor(message) { this.message = message; this.name = "Error"; // (different names for different built-in error classes) this.stack = <nested calls>; // non-standard, but most environments support it } }
Now let’s go on and inherit ValidationError
from it:
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 }
Please take a look at the constructor:
super
in the child constructor, so that’s obligatory. The parent constructor sets the message
property.name
property to "Error", so in the line (2) we reset it to the right value.Let’s try to use it in readUser(json):
class ValidationError extends Error { constructor(message) { super(message); this.name = "ValidationError"; } } // Usage function readUser(json) { let user = JSON.parse(json); if (!user.age) { throw new ValidationError("No field: age"); } if (!user.name) { throw new ValidationError("No field: name"); } return user; } // Working example with try..catch try { let user = readUser('{ "age": 25 }'); } catch (err) { if (err instanceof ValidationError) { alert("Invalid data: " + err.message); // Invalid data: No field: name } else if (err instanceof SyntaxError) { // (*) alert("JSON Syntax Error: " + err.message); } else { throw err; // unknown error, rethrow it (**) } }
The try..catch
block in the code above handles both our ValidationError
and the built-in SyntaxError
from JSON.parse
.
Please take a look at how we use instanceof
to check for the specific error type in the line (*).
We could also look at err.name
, like this:
// ... // instead of (err instanceof SyntaxError) } else if (err.name == "SyntaxError") { // (*) // ...
The instanceof
version is much better, because in the future we are going to extend ValidationError
, make subtypes of it, like PropertyRequiredError
. And instanceof
check will continue to work for new inheriting classes. So that’s future-proof.
Also it’s important that if catch
meets an unknown error, then it rethrows it in the line (**). The catch
only knows how to handle validation and syntax errors, other kinds (due to a typo in the code or such) should fall through.
The ValidationError
class is very generic. Many things may go wrong. The property may be absent or it may be in a wrong format (like a string value for age). Let’s make a more concrete class PropertyRequiredError
, exactly for absent properties. It will carry additional information about the property that’s 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 readUser(json) { let user = JSON.parse(json); if (!user.age) { throw new PropertyRequiredError("age"); } if (!user.name) { throw new PropertyRequiredError("name"); } return user; } // Working example with try..catch try { let user = readUser('{ "age": 25 }'); } catch (err) { if (err instanceof ValidationError) { alert("Invalid data: " + err.message); // Invalid data: No property: name alert(err.name); // PropertyRequiredError alert(err.property); // name } else if (err instanceof SyntaxError) { alert("JSON Syntax Error: " + err.message); } else { throw err; // unknown error, rethrow it } }
The new class PropertyRequiredError
is easy to use: we only need to pass the property name: new PropertyRequiredError(property)
. The human-readable message is generated by the constructor.
Please note that this.name
in PropertyRequiredError
constructor is again assigned manually. That may become a bit tedious – to assign this.name = <class name>
when creating each custom error. But there’s a way out. We can make our own “basic error” class that removes this burden from our shoulders by using this.constructor.name
for this.name
in the constructor. And then inherit from it.
Let’s call it MyError
.
Here’s the code with MyError
and other custom error classes, simplified:
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
Now custom errors are much shorter, especially ValidationError, as we got rid of the "this.name = ..."
line in the constructor.