Better error handling in JavaScript

How and why to use custom error types in JavaScript

Iain Collins
Aug 13, 2018 · 6 min read

Handling errors well can be tricky. How Error() historically worked in JavaScript hasn’t made this easier, but using the Error class introduced in ES6 can be helpful

Throwing errors in JavaScript

JavaScript supports try/catch/finally but the Error object it uses works differently from Exceptions in other languages and it doesn’t provide a way to catch Errors by type in the same way you can with Exceptions in Java or C# so it’s quite common for all Errors thrown in a project to be instances of Error.

Some vendors have implemented a conditional catch clause but it’s not a standard and not widely supported in browsers.

As a result of not having a standard way to define errors — or to return them when throwing them from a server to a client — information about an error being returned is often lost, with projects featuring bespoke error handling.

Throwing errors in other languages

If you’ve ever used .NET/Mono you may appreciate how good the error handling behaviour is out of the box, especially for web services.

If you are not familiar with the .NET/Mono stack bare with me here for a minute, we’ll be coming back to how it’s relevant below.

Naming conventions

.NET has naming conventions for errors, like InputValidationException for input validation errors and SecurityException for permissions errors, which is great for encouraging consistency in error handling.

Serializing errors

Another great feature of .NET is that throwing an exception on the server in a web service automatically can automatically throw that exception in a client using the service — whether the client is C#, Java, PHP or JavaScript.

This is possible thanks to the Web Services Description Language (WSDL), which defines how objects, including Exceptions, are serialized and has native support in some languages (including C# and PHP) and support via third party libraries in others (including Java and JavaScript).

Benefits of having a convention

What’s so great about having conventions for errors is it makes it easier to handle specific errors well because they have explicit and consistent types.

With pre-defined error types you can easily decided how much detail you want to return back to the client (such as the names of fields that failed validation, and what was wrong with them) while logging additional stack trace information on the server when unexpected errors are triggered.

You can also add specific properties to an error, to make it easier to highlight where a problem is — such as an input field with an invalid value.

Serialisation is automatic and consistent across an application, including between clients and servers. This makes it easier to handle errors well server side and in a user interface.

Defining error types in JavaScript

JavaScript actually has a several core Error types by default, but they are quite niche and of limited usefulness for error handling in most applications.

However, with ES6 you can extend the Error class and define custom errors with their own behaviour — such as logging errors automatically – and you can choose what detail to add or include when returning an error.

You can define each class in a file or – if you only have a small number of error types, which is probably the case for most projects – you can define them all in a single file as named exports.

An example of exporting different Error types as named exports:

class ValidationError extends Error {
constructor(message) {
super(message)
this.name = 'ValidationError'
this.message = message
}
}
class PermissionError extends Error {
constructor(message) {
super(message)
this.name = 'PermissionError'
this.message = message
}
}
class DatabaseError extends Error {
constructor(message) {
super(message)
this.name = 'DatabaseError'
this.message = message
}
}
module.exports = {
ValidationError,
PermissionError,
DatabaseError
}

You can use custom Errors in your code just like you would a normal Error:

const { ValidationError } = require('./error')function myFunction(input) {
if (!input)
throw new ValidationError('A validation error')
return input
}

Note: This example is for Node.js and uses ‘require’. If you were writing it for ES6 in the browser (or using Babel for isomorphic code) you would write the include statement as import { ValidationError } from './error'

When you throw a custom error, you can check what type is (e.g. by looking at the value of .name) and decide how to handle it accordingly:

try {
myFunction(null)
} catch (e) {
if (e.name === 'ValidationError') {
console.log("Handle input validation error")
} else {
console.log("Handle other errors")
}
}

Defining a name property, a standard property of the Error class in JavaScript, provides a way to easily check the Error type if it’s serialized into a standard object in a REST call or in a socket service callback, so you can throw the Error all the way up the stack—e.g. from an internal method, to web server route handler and all the way to a browser —and still know what happened.

If you are returning an error from a REST or socket based service, the Error will usually be serialised into JSON and back, and is likely to be transformed into a plain object (and no longer be an Error object) by the time the client evaluates the response, but defining error types like this in your projects can still help provide a convention for returning and checking for errors.

Returning Error objects from Promises

You can use custom Error objects inside a Promise too.

It’s best to avoid throwing errors from inside a Promise, because they may not always be caught, depending on how the code that called them is structured.

However it’s good practice to return an error when rejecting a Promise, and you can return Error custom types just like any other Error.

const { ValidationError } = require('./error')function myFunction(input) {
return new Promise((resolve, reject) => {
if (!input)
return reject(new ValidationError('A validation error'))
resolve(input)
})
}

When calling the function, your catch() clause can then check to see the response when an error is returned. If you return an Error instance (or a class that extends it) you will have a full stack trace of the error that was thrown.

Any code already using this function and that was expecting an Error object will be compatible with a custom Error that extends the default Error class.

Serializing Error objects in JSON

By default, Error objects seralize to JSON with output like this:

{ name: 'ValidationError' }

This output is not particularly helpful if you want to pass errors back directly to a web front end from a framework like Express or Socket.IO.

You can can override the serialization method to return a different response:

class ValidationError extends Error {
constructor(message) {
super(message)
this.name = 'ValidationError'
this.message = message
}

toJSON() {
return {
error: {
name: this.name,
message: this.message,
stacktrace: this.stack
}
}
}
}

This example returns the response encapsulated in a property called ‘error’, making it easier to check for in responses. It seralizes to JSON like this:

{ 
error: {
name: 'ValidationError',
message: 'A validation error',
stacktrace: '…'
}
}

If you have multiple error types, you might want to create your own custom error class that extends Error and then base all your error classes off that.

Using HTTP status codes is appropriate on HTTP services, but having a standard format for errors in JSON is still helpful – especially in other contexts, like responses sent over a socket connection.

You may not want to include a stack trace (with file names and line numbers) in production, but it can be helpful in development and testing and you can use a conditional so they are only returned in development.

Note: To test seralization, you can use JSON.stringify() on an Error instance:
console.log(JSON.stringify(new ValidationError(‘A validation error’)))

Summary

Taking a cue from how Error handling works in other languages like Java and C# and defining custom Errors with standard methods for seralizing them requires doesn’t require much code to implement and provides a consistent and easily understandable convention to follow.

Establishing good error handling conventions in a project can make it easier to improve the user experience of your software, squash mysterious ‘unknown error’ messages, track down causes of unexpected behaviour and makes it easier to log, monitor and report on errors.

Iain Collins

Written by

News and media, civic tech and software. Cat herder at The Economist. Director at Glitch Digital.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade