Elegant javascript error handling

A readable and maintainable pattern for handling javascript errors.

Javascript Maintenance
17 March 2020

Javascripts async await API offers much to free us from callback hell. The unfortunate cost is the readability of try catch. In this article I'll explain the advantages of returning Error objects instead of throwing.

Throwing means nesting function calls in try catch restriciting block scope to the try block. To remedy this a variable must be initialized beforehand. Seperating definition from declaration makes for one more thing to look up and of course we lose the use of const.

Instead we return an Error object and check the return values type:

const data = await parseUrl(url);

if (data instanceof Error) {
// Handle error or propagate it by returning it
}

This pattern is similiar to both old callback style javascript and Golang. Alternatively you could return an array with an optional error in the first position closely mirroring JS callbacks.

const [optionalError, data] = await parseUrl(url);

if (optionalError) {
// Handle error or propagate it by returning it
}

Compared to try catch:

let data;

try {
data = await parseUrl(url);
} catch (error) {
// Handle error
}

Tip: Creating new errors can be shortened to one line using a custom error class. This also makes for easy identification of your errors:

// lib/ApplicationError.js
class ApplicationError extends Error {
constructor(message, code) {
super(message);
this.code = code;
}
}

// lib/app.js
import ApplicationError from 'lib/ApplicationError';

if (!isValid(url)) {
return new ApplicationError('Cannot parse, url is invalid', 'INVALID_URL');
}

Should you ever throw an error?

You can still throw when it's appropriate but often you simply need to reject and short circuit a function due to cases such as invalid data, not because something has gone wrong. Think status code 422 - return early, don't drop the connection.


Full Example:

// lib/ApplicationError.js
class ApplicationError extends Error {
constructor(message, code) {
super(message);
this.code = code;
}
}

// lib/app.js
import ApplicationError from 'lib/ApplicationError';

function parseUrl(url) {
if (!isValid(url)) {
return new ApplicationError('Cannot parse, url is invalid', 'INVALID_URL');
}
}

const data = parseUrl(url);

if (data instanceof Error) {
// Handle error or propagate it by returning it
}

// Use data