In JavaScript, there’s an Error
class to let code throw exceptions. We can create custom error classes by extending the Error
class with our own class and custom logic to throw more specific errors.
The Error
class has the message
, name
, and stack
files that we inherit from. Of course, like any other class, we can define our own fields and methods inside it.
Create a New Error Class
For example, to make data validation easier, we can make a ValidationError
class which is thrown whenever our code encounters invalid data.
We can create a new class as follows:
class ValidationError extends Error {
constructor(message, type) {
super(message);
this.name = "ValidationError";
this.type = type;
}
}
As we can see, we used the extends
keyword to indicate that we want to inherit from the Error
class.
Using Error Classes we Defined
After we defined our error classes, we can use our ValidationError
class as follows:
try {
let data = {
email: 'abc'
}
if (!/[^@+@[^.+..+/.test(data.email)) {
throw new ValidationError('invalid email', 'email error');
}
} catch (ex) {
console.log(ex);
}
In the console.log
output, we should see ‘ValidationError: invalid email’ when we run the code.
We can also log the message
, name
, stack
, and type
properties individually:
try {
let data = {
email: 'abc'
}
if (!/[^@]+@[^.]+..+/.test(data.email)) {
throw new ValidationError('invalid email', 'email error');
}
} catch (ex) {
const {
message,
name,
stack,
type
} = ex;
console.log(
message,
name,
stack,
type
);
}
Then we should see:
invalid email
logged formessage
ValidationError
logged forname
ValidationError: invalid email at window.onload
forstack
email error
logged fortype
As with another type of object, we can use the instanceof
operator to check if it’s an instance of ValidationError
as follows:
try {
let data = {
email: 'abc'
}
if (!/[^@]+@[^.]+..+/.test(data.email)) {
throw new ValidationError('invalid email', 'email error');
}
} catch (ex) {
if (ex instanceof ValidationError){
console.log('validation error occurred');
}
}
Then we should see ‘validation error occurred’
logged since we threw an instance of the ValidationError
class.
We can also look at the name
property to discern different kinds of errors. For example, we can write:
try {
let data = {
email: 'abc'
}
if (!/[^@]+@[^.]+..+/.test(data.email)) {
throw new ValidationError('invalid email', 'email error');
}
} catch (ex) {
if (ex.name === 'ValidationError') {
console.log('validation error occurred');
}
}
Then we get the same thing as above.
Wrapping Exceptions
We can wrap lower-level exceptions, which are of instances of a subclass of a parent exception constructor and create higher-level errors.
To do this, we can catch and rethrow lower-level exceptions and catch them in higher-level code.
First, we have to create a parent and child classes which extend from the Error
class as follows:
class FormError extends Error {
constructor(message, type) {
super(message);
this.name = "FormError";
}
}
class ValidationError extends FormError {
constructor(message, type) {
super(message);
this.name = "ValidationError";
this.type = type;
}
}
In the code above, the FormError
class extends the Error
class and act as the parent class of the ValidationError
class.
Then we write some code which makes use of our exception classes that we created:
const checkEmail = (data) => {
try {
if (!/[^@]+@[^.]+..+/.test(data.email)) {
throw new ValidationError('invalid email', 'email error');
}
} catch (ex) {
if (ex instanceof ValidationError) {
throw new FormError(ex.name)
} else {
throw ex;
}
}
}
try {
let data = {
email: 'abc'
}
checkEmail(data);
} catch (ex) {
if (ex instanceof FormError) {
throw new FormError(ex.name)
} else {
throw ex;
}
}
In the code above, we have a checkEmail
function that throws an error if the data.email
property has an invalid email. If a ValidationError
is thrown, then we throw a new error.
Then we create a try...catch
block after the checkEmail
function to wrap around the function call and then we catch the exception in the catch
block. We check for FormError
instances and then throw a FormError
if the caught exception is an instance of the FormError
.
Otherwise, we rethrow the caught exception directly. We have this pattern so that we only deal with one kind of error at the top level, instead of checking for all kinds of error in top-level code.
The alternative is to have if...else
statements to check for all kinds of errors at the top-level, which is more complex.
We can inherit from the Error
class to create our own error classes. Then we can throw new error instances with the throw
keyword.
In the class we define, we can add our own instance fields and methods.
When we catch the errors, we can use the insranceof
operator to check for the type of error that’s thrown. We can also use the name
property to do the same thing.
In our lower level code, we can catch errors of type of the sub-class error type and then rethrow an error of the instance of the parent class. Then we can catch the error of the higher-level type instead of checking for error instances of the lower-level type.