Elegant javascript error handling

A readable and maintainable pattern for handling javascript errors.

Javascripts async await API improved code readability, try catch less so. In error cases it's much cleaner to return an Error object, keep throwing for exceptional cases. This pattern is similiar to those of both Golang and old callback style javascript.

Example:

function parseUrl(url) {
if (!isValid(url)) {
const error = new Error('Cannot parse, url is invalid');
error.code = 'INVALID_URL';
return error;
}
}

// Check for returned errors
const data = parseUrl(url);

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

// Use data

A custom error class can be used to shorten the creation new errors to a single line

// 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');
}

Why returning Error objects is better than throwing

Throwing requires nesting in a try catch block, making your code less clear and limiting your scope. To remedy this let must be used instead of const to make returned data accessable outside the try catch block.

It doesn't break the rule "Exceptions should be exceptional". 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, don't drop the connection.

function doSomething(url) {
if (!url.startsWith('https')) {
const error = new Error('Cannot parse, protocol must be https');
error.code = 'INVALID_PROTOCOL';
throw error;
}
}

// Check for returned errors
let data;

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

// Use data