In development, we are writing code that performs some tasks. These tasks are the product features that our users will use. But things may go wrong in our tasks. We can face problems with requests, in our database, a user is asking for something that does not exist or he doesn’t have permission to access it, etc… That’s why we need to know that something is going wrong!
And at this point, we need to know how to raise an error properly!
Our errors should have some properties like message, name, and stack (from which file the error is coming?). They also can have other properties like statusCode such as 404 for Not Found, 403 for Forbidden, 400 for Bad Request…
Extending error
Let’s create a function called readBook(JSON) that reads the JSON data of a book. This is how a valid book data looks like:
let json = `{ "title": "Deep Work", "pages": 296 }`;
We will parse the JSON, if the JSON is malformed we will throw an error called SyntaxError. If the JSON format is valid, we will check the JSON content validity, an invalid data is data that does not contain a title and page properties.
Our function readBook will read and check the data, if the format is correct it will check the JSON properties, if we don’t have title and page properties, we will throw a ValidationError.
Our ValidationError class should inherit from the built-in Error class.
// 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 = ; // non-standard, but most environments support it
}
}
Now let’s inherit ValidationError from it and try it in action:
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
function test() {
throw new ValidationError("This is a validation error!");
}
try {
test();
} catch(err) {
alert(err.message); // This is a validation error!
alert(err.name); // ValidationError
alert(err.stack); // a list of nested calls with line numbers for each
}
Now let’s use our validation error with readBook:
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
// Usage
function readBook(json) {
let book = JSON.parse(json);
if (!book.pages) {
throw new ValidationError("No field: pages");
}
if (!user.title) {
throw new ValidationError("No field: title");
}
return book;
}
// Working example with try..catch
try {
let book = readBook('{ "pages": 25 }');
} catch (err) {
if (err instanceof ValidationError) {
alert("Invalid data: " + err.message); // Invalid data: No field: title
} else if (err instanceof SyntaxError) { //
alert("JSON Syntax Error: " + err.message);
} else {
throw err; // unknown error, rethrow it
}
}
1- We created our ValidationError that inherits Error properties.
2- Inside our function readBook, we first parsed the coming data with JSON.parse, then we checked for the existence of the two properties title and pages which are mandatory to have a valid book data.
3- If one of these properties is missing, we throw a Validation Error with the convenient message.
4- Inside the try… catch block, we’re passing an invalid book data to readBook function. If something is wrong the error will be thrown by readBook and handled in the catch block.
5- In the catch block we’re checking the error type with instanceof operator which is used to “check whether an object belongs to a certain class. It also takes inheritance into account.” and based on our error type we’re going to see an appropriate message.
Additional inheritance
Valid data error is very generic, we also may have many other kinds of errors related to validation such as the wrong format for the property; we’re expecting pages to be always an integer but what if we receive a string? So, we need to be more concrete and handle every potential error.
For this, we’re going to create a class for only errors related to missing properties. This class will inherit ValidationError properties:
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
class MissingPropertyError extends ValidationError {
constructor(property) {
super("No property: " + property);
this.name = "MissingPropertyError";
this.property = property;
}
}
// Usage
function readBook(json) {
let book = JSON.parse(json);
if (!book.title) {
throw new MissingPropertyError("title");
}
if (!book.pages) {
throw new MissingPropertyError("pages");
}
return book;
}
// Working example with try..catch
try {
let book = readBook('{ "pages": 25 }');
} catch (err) {
if (err instanceof ValidationError) {
alert("Invalid data: " + err.message); // Invalid data: No property: title
alert(err.name); // MissingPropertyError
alert(err.property); // title
} else if (err instanceof SyntaxError) {
alert("JSON Syntax Error: " + err.message);
} else {
throw err; // unknown error, rethrow it
}
}
This code looks like the previous one, but what’s different here is that we only need to throw the error with throw new MissingPropertyError(missingProperty) and pass as a parameter the missing property. The error message will be generated from the constructor.
What is remarkable and common between all our custom error classes is that we’re manually assigning this.name and we have to do that in every custom error class. We can overcome this by creating our basic error class that assigns this.name = this.constructor.name and inherits all our custom errors from it.
class MyCustomError extends Error {
constructor(message) {
super(message);
this.name = this.constructor.name;
}
}
class ValidationError extends MyCustomError { }
class MissingPropertyErrorextends ValidationError {
constructor(property) {
super("No property: " + property);
this.property = property;
}
}
// check if the name is correct
alert( new MissingPropertyError("field").name ); // MissingPropertyError
With MyCustomError we will always have the right name for every custom error class we create without the need of assigning it manually in every constructor.
Wrapping exception
For our readBook function, we have created three types of errors: SyntaxError which checks for the JSON format validity, ValidationError which checks the data. Our function may extend and it can then generate many other types of errors that we haven’t created classes for, but we should handle them in all cases.
We will do this by having many conditional statements in the catch block (if…else) that check the class and handle known errors and rethrow the unknown ones.
try {
...
readBook() // the potential error source
...
} catch (err) {
if (err instanceof ValidationError) {
// handle validation errors including MissingPropertyError
} else if (err instanceof SyntaxError) {
// handle syntax errors
} else {
throw err; // unknown error, rethrow it
}
}
What if we had many error types, do we need to check for every single error in an if statement? Absolutely not, we need to get details about the error only if we need to.
We’re going to use a technique called “wrapping exceptions”.
1- We will make a class ErrorDetails to represent errors related to data reading.
2- Data reading errors are SyntaxError and ValidationError, we will catch these errors inside our readBook function and generate an ErrorDetails.
3- This generated error will be our reference to the originally generated error and its cause (we will pass the error message and cause as parameters for every ErrorDetails instance created).
class ErrorDetails extends Error {
constructor(message, cause) {
super(message);
this.cause = cause;
this.name = 'ErrorDetails';
}
}
class ValidationError extends Error { /*...*/ }
class MissingPropertyError extends ValidationError { /* ... */ }
function validateBook(book) {
if (!book.pages) {
throw new MissingPropertyError("pages");
}
if (!book.title) {
throw new MissingPropertyError("title");
}
}
function readBook(book) {
let book;
try {
book = JSON.parse(json);
} catch (err) {
if (err instanceof SyntaxError) {
throw new ErrorDetails("Syntax Error", err);
} else {
throw err;
}
}
try {
validatebook(book);
} catch (err) {
if (err instanceof ValidationError) {
throw new ErrorDetails("Validation Error", err);
} else {
throw err;
}
}
}
try {
readBook('{bad json}');
} catch (e) {
if (e instanceof ErrorDetails) {
alert(e);
// Original error: SyntaxError: Unexpected token b in JSON at position 1
alert("Original error: " + e.cause);
} else {
throw e;
}
}
Feel free to play around with the code and try it in your JavaScript projects, you won’t learn until you get your hands dirty.