1.4K Followers
·
Follow

Better error handling in JavaScript

How and why to use custom error types in JavaScript

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

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 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

Serializing errors

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

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

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

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

{ 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

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.

Written by

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

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store