Using Custom Errors in Node.js
Once you use them, you'll never go back
The Error object is an often skipped item in JavaScript. This tutorial will introduce you to three important aspects of creating and handling errors in Node.js:i
- Why use the Error object?
- How to create custom error classes
- Examples of when you can use custom errors
Why Use the Error Object?
Often people don’t have a strategy to handle errors in their applications. They ebb on the side of optimism and use console.log('error') while debugging. We can save ourselves, our future selves, and our coworkers valuable time with some very small changes.
Strings Are Not Errors
Guillermo Rauch wrote an excellent article on why strings should never be used in lieu of Error objects. It’s typical to find people returning a string to indicate an error:
if (id < 0) return 'id must be positive'
This is shortsighted for a few reasons:
What if you want to see where the error happened?
Unless you include the file name and line number in the string, there’s no way of knowing where the error happened. This is tedious and hard to maintain.
What if you want to handle different types of errors?
If all you have are strings, at runtime you don’t know what type of error happened. It could be something you want to show a user: incorrect information entered in a form, or something you want to log (for example, an error in a database query).
The Error Object
Let’s replace the error strings above with the Error object:
if (id < 0) return new Error('id must be positive')
Although slightly more verbose, this approach has multiple benefits:
What if you want to see where the error happened?
By using the error’s stack property we can get the exact file and line number the error was instantiated on:
console.log(error.stack)
Error: An error occurred
at Object.<anonymous>(/Users/ds/main.js:1:73)
at Module._compile (module.js:441:26)
at Object..js (module.js:459:10)
at Module.load (module.js:348:31)
at Function._load (module.js:308:12)
at Array.0 (module.js:479:10)
at EventEmitter._tickCallback (node.js:192:40)
What if you want to handle different types of errors?
We can use instanceof to check for the type of error received and handle them accordingly:
if (err instanceof DatabaseError) { /* do this */ }
if (err instanceof AuthenticationError) { /* do that */ }
Takeaway: Using the native Error object enables quicker debugging and allows you to check error types.
How to Create Custom Errors
Now that you know the benefits of using Error objects instead of strings, let’s take it a step further and create our own error types.
Abstract Error
We’ll start by creating an AbstractError class that our custom Errors will extend:ii
var util = require('util')
var AbstractError = function (msg, constr) {
Error.captureStackTrace(this, constr || this)
this.message = msg || 'Error'
}
util.inherits(AbstractError, Error)
AbstractError.prototype.name = 'Abstract Error'
Let’s break down what’s happening:
// Grab the util module that's bundled with Node
var util = require('util')
// Create a new Abstract Error constructor
var AbstractError = function (msg, constr) {
// If defined, pass the constr property to V8's
// captureStackTrace to clean up the output
Error.captureStackTrace(this, constr || this)
// If defined, store a custom error message
this.message = msg || 'Error'
}
// Extend our AbstractError from Error
util.inherits(AbstractError, Error)
// Give our Abstract error a name property. Helpful for logging the error later.
AbstractError.prototype.name = 'Abstract Error'
Database Error
Now that we have an AbstractError class, we can extend it with other custom error classes as we see fit:
var DatabaseError = function (msg) {
DatabaseError.super_.call(this, msg, this.constructor)
}
util.inherits(DatabaseError, AbstractError)
DatabaseError.prototype.message = 'Database Error'
We’ve created a new DatabaseError that extends AbstractError. In DatabaseError’s constructor, we call its superclass’ constructor with the msg parameter and a reference to the DatabaseError’s constructor. The purpose of this is threefold: To complete the prototype inheritance given to us by util.inherits, pass along the optional message, and simplify the error’s stack output.
Putting Our Errors to Use
The following examples use Node’s conventions for asynchronous method calls. A method accepts n parameters followed by a callback. If an error is raised, return the error in the callback’s first parameter; otherwise, return null in the first parameter and the response data in the second.iii Due to node’s asynchronous nature we can’t use try…catch blocks like you see in other languages.iv
function getUserById(id, callback) {
if (!id) {
return callback(new Error('Id is required'))
}
// Let’s pretend our database breaks if we try to
// find a user with an id higher than 10
if (id > 10) {
return callback(new DatabaseError(Id can't be higher ↵ than 10))
}
callback(null, { name: 'Harry Goldfarb' })
}
function onGetUserById(err, resp) {
if (err) {
return console.log(err.toString())
}
console.log('Success:', resp.name)
}
getUserById(1, onGetUserById) // Harry Goldfarb
getUserById(null, onGetUserById) // Error: Id is required
getUserById(53, onGetUserById)
// Database Error: Id can't be higher than 10
Wonderful. Our getUserById method now returns two types of errors, the native Error and our custom DatabaseError. If we call toString(), we can see which type of error was returned.
Now let’s handle DatabaseErrors differently than native Errors:
// start our script in production mode
$ NODE_ENV=production node main.js
function onGetUserById(err, resp) {
if (err) {
if (err instanceof DatabaseError &&
process.env.NODE_ENV != 'production') {
return console.log(err)
}
return console.log('Sorry there was an error')
}
console.log(resp.name)
}
The above example is contrived as you would want to catch database errors closer to the service or model level. It was used to show what is possible.
Takeaway: Creating custom errors is simple and enables us to check at run time their type. They also preserve the stack property which they inherited from Error.
Reusing Our Errors
We don’t want to declare our custom errors in every file they are used so let’s create a file that we can import using node’s require() method.
// in ApplicationErrors.js
var util = require('util')
var AbstractError = function (msg, constr) {
Error.captureStackTrace(this, constr || this)
this.message = msg || 'Error'
}
util.inherits(AbstractError, Error)
AbstractError.prototype.name = 'Abstract Error'
var DatabaseError = function (msg) {
DatabaseError.super_.call(this, msg, this.constructor)
}
util.inherits(DatabaseError, AbstractError)
DatabaseError.prototype.name = 'Database Error'
module.exports = {
Database: DatabaseError
}
// in main.js
var ApplicationError = require('./ApplicationErrors')
function getUserById(id, callback) {
if (!id) {
return callback(new Error('Id is required'))
}
if (id > 10) {
return callback(new ApplicationError.Database('Id cant ↵ be higher than 10'))
}
callback(null, { name: 'Harry Goldfarb' })
}
Now we’re able to require our CustomErrors in any file in our application.
Wrapping it up
There’s multiple reasons to use the native Error object over strings in your application. Using custom errors allows further precision when handling and creating errors.