Node.js is a popular runtime to write apps for. These apps are often production quality apps that are used by many people. To make maintaining them easier, we have to set some guidelines for people to follow.
In this article, we’ll look at the best practices for error handling in Node.js apps.
Use only the built-in Error object
In JavaScript, we can create a subclass of the error object, so there’s no need to create a custom type or return some error value to throw errors.
We can either instantiate the Error
class or a subclass of the Error
class. This way, it’ll increase uniformity and decrease confusion. It’s much less headache for everyone reading the code.
For example, we can throw an Error
instance by writing:
throw new Error('error');
We can catch errors and handle them gracefully by using the catch
block as follows:
try {
foo();
} catch (e) {
if (e instanceof RangeError) {
console.error(`${e.name} - ${e.message}`)
}
}
The code above only handles RangeError
instances.
We can get the error name and stack trace from the Error
instance. Also, we can create our own error class by subclassing the Error
class:
class CustomError extends Error {
constructor(foo = 'bar', ...params) {
super(...params)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, CustomError)
}
this.name = 'CustomError'
this.foo = foo
this.date = new Date()
}
}
Then we can throw a CustomError
instance and catch it by writing:
try {
throw new CustomError('baz');
} catch (e) {
if (e instanceof CustomError) {
console.error(`${e.name} - ${e.foo}`)
}
}
Distinguish Operational vs Programmer Errors
Operational errors by the user should be handled by the user. For instance, if invalid input is entered, we should handle it by sending them an error message.
On the other hand, if the error is a programmer error, then we should fix them. Keeping the programmer error unfixed isn’t a good idea since it leads to bad user experience and data corruption.
For user errors, we should send the client 400 series response codes for web apps. 400 is for bad input, 401 for unauthorized, and 403 for forbidden.
Handle Errors Centrally, Not within an Express Middleware
In Express apps, error handling should be done centrally in its own middleware rather than spread out in multiple middlewares.
For instance, we should write something like the following code to handle errors:
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.get('/', (req, res, next) => {
try {
throw new Error('error')
res.send('hello')
} catch (err) {
next(err)
}
});
app.use((err, req, res, next) => {
res.send('error occurred')
})
app.listen(3000, () => console.log('server started'));
In the code above, we added an error handler to the end of the app so that it’ll run when all routes call next
.
This way, we don’t have to write multiple pieces of code to handle errors. In the code above:
app.use((err, req, res, next) => {
res.send('error occurred')
})
is the error handler middleware. The next
function in the GET route calls the error handler since it threw an error.
Document API errors using Swagger
Documentation is very important since it lets people know how to use the API and make sure that changes to the API won’t accidentally create undesirable behavior.
For instance, adding Swagger to an Express app is easy since we have the Swagger UI Express package to do it for us. We can install the Swagger UI Express package by writing:
npm install swagger-ui-express
Then we can create a swagger.json
file as follows:
{
"openapi": "3.0.0",
"info": {
"title": "Sample API",
"description": "Optional multiline or single-line description in [CommonMark](http://commonmark.org/help/) or HTML.",
"version": "0.1.9"
},
"servers": [
{
"url": "https://UnwittingRudeObservation--five-nine.repl.co",
"description": "Optional server description, e.g. Main (production) server"
},
{
"url": "https://UnwittingRudeObservation--five-nine.repl.co",
"description": "Optional server description, e.g. Internal staging server for testing"
}
],
"paths": {
"/": {
"get": {
"summary": "Returns a hello message.",
"description": "Optional extended description in CommonMark or HTML.",
"responses": {
"200": {
"description": "A JSON string message",
"content": {
"application/json": {
"schema": {
"type": "string",
"items": {
"type": "string"
}
}
}
}
}
}
}
}
}
}
Then in our app, we can write:
const express = require('express');
const bodyParser = require('body-parser');
const swaggerUi = require('swagger-ui-express');
const swaggerDocument = require('./swagger.json');
const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.get('/', (req, res, next) => {
res.send('hello')
});
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));
app.listen(3000, () => console.log('server started'));
We can edit our Swagger API documentation by going to https://editor.swagger.io/.
Conclusion
If we want to throw errors in our Node app, we should throw an instance of Error
or subclasses of Error
. Catching errors can be done in the catch block for Node apps. It should also be handled by a central error handler for Express apps.
To make using and changing the API easy, we should use Swagger to document the routes that our API has. For Express apps, we can use Swagger UI Express to serve our swagger.json
file in an easy to read format as a separate page.
For user errors, we should send errors back to them. Programmer errors should be fixed by us.