Custom Errors in Node.js
I'm surprised how little custom errors are used in Node.js and I don't understand why.
I was integrating an open source library last week into a project and I was trying to write unit tests covering all failure cases one could expect with the library. The library, however, didn't make use of explicit error cases. This left me having to parse the error message to determine what type of error I was experiencing:
expect(() => {
executeWithInvalidInput(['foo', 'bar', 'hello', 'world']);
}).to.throw(/Invalid input: "foo" should have come after "bar"/);
I see two distinct issues with throwing a generic Error
:
- Cannot differentiate errors by object type (prototype hierarchy).
- Feedback returned by error is limited to the
Error.message
.
Using the previous example, what if we could instead?:
try {
executeWithInvalidInput(['foo', 'bar', 'hello', 'world']);
expect.fail();
} catch (error) {
expect(error).to.be.an.instanceOf(InvalidInputOrderError);
expect(error.data.violator).to.eq('foo');
}
This version of the test case is much more specific and has the added benefit of verifying the exact input that caused the problem. It also prevents the exact nature of the error from being hidden. In the case of that third party library, their test cases were not actually catching the right error: https://github.com/marcelklehr/toposort/issues/24.
So how do we create a custom error in JavaScript?
Later versions of Node.js make the process simple by allowing you to extend the Error class:
class DomainError extends Error {
constructor(message) {
super(message);
// Ensure the name of this error is the same as the class name
this.name = this.constructor.name;
// This clips the constructor invocation from the stack trace.
// It's not absolutely essential, but it does make the stack trace a little nicer.
// @see Node.js reference (bottom)
Error.captureStackTrace(this, this.constructor);
}
}
class ResourceNotFoundError extends DomainError {
constructor(resource, query) {
super(`Resource ${resource} was not found.`);
this.data = { resource, query };
}
}
// I do something like this to wrap errors from other frameworks.
// Correction thanks to @vamsee on Twitter:
// https://twitter.com/lakamsani/status/1035042907890376707
class InternalError extends DomainError {
constructor(error) {
super(error.message);
this.data = { error };
}
}
module.exports = {
ResourceNotFoundError,
InternalError,
};
Using custom errors are as natural as generic ones and stylistically look much better:
const Person = require('../models/person');
const { ResourceNotFoundError, InternalError } = require('../errors');
async function findPerson(name) {
const query = { name };
let person;
try {
person = await Person.find(query);
} catch (error) {
throw new InternalError(error);
}
if (!person) {
throw new ResourceNotFoundError('person', query);
}
return person;
}
module.exports = findPerson;
Now when I test the function, I can ensure that unexpected error cases aren't hidden by the test validation. For instance, if an Internal Error occurs, it will not be hidden by the generic test case:
const { describe, it } = require('mocha');
const { expect } = require('chai');
const { ResourceNotFoundError, InternalError } = require('../lib/errors');
describe('when looking up People', () => {
it('should throw an error if the person cannot be found', async () => {
expect(() => await findPerson('John Doe'))
.to.throw(ResourceNotFoundError);
});
});
I also find this to be an excellent strategy for mapping domain errors to protocol ones (keeping your business layer clean). Here is an example using the strategy with Hapi.js:
// Boom is a Hapi framework for creating HTTP error responses.
const Boom = require('boom');
const { ResourceNotFoundError, InternalError } = require('../lib/errors');
const findPerson = require('../lib/people/find');
// This would be a common utility function used across handlers
function mapDomainErrorToHttpResponse(error) {
if (error instanceof ResourceNotFoundError) {
return Boom.notFound(error.message, error.data.query);
}
return Boom.badImplementation('Internal Error');
}
server.route({
method: 'GET',
path: '/people/{name}',
async handler(request, reply) {
try {
reply(await findPerson(request.params.name));
} catch (error) {
reply(mapDomainErrorToHttpResponse(error));
}
},
});
Hopefully, you will agree that custom errors are worth the effort. They don't require a lot of extra code and make the intent of your error extremely clear. If you are developing a public facing API, don't be afraid of exporting those error types so consumers can catch them. I promise you the explicit structure of the error will be appreciated by your users.
Note: I mention the marcelklehr/toposort library as an example that inspired this post, but I hope you don't consider this a criticism of the library or author. I'm extremely grateful for his (and fellow contributors) work -
toposort
provides essential functionality for an upcoming DI framework project I'm working on.
Resources
Node.js Error reference: https://nodejs.org/api/errors.html
Node.js Error.capatureStackTrace: https://nodejs.org/api/errors.html#errors_error_capturestacktrace_targetobject_constructoropt
This Gist inspired my original Error implementation: https://gist.github.com/slavafomin/b164e3e710a6fc9352c934b9073e7216
You might also be interested in these articles...
Stumbling my way through the great wastelands of enterprise software development.