Categories
Express Node.js

How to create an error handler for your Express API

7 min read

Express provides a default error handler, which seems great until you realise that it will only send an HTML formatted error response. This is no good for your API as you want it to always send JSON formatted responses. You start handling errors and sending error responses directly in your Express route handler functions.

Before you know it, you have error handling code that’s logging errors in development to help you debug, and doing extra handling of the error object in production so you don’t accidentally leak details about your application’s internals. Even with just a few routes, your error handling code is getting messy, and even worse, it’s duplicated in each of your route handler functions. Argh!

Wouldn’t it be great if you could send JSON error responses from your API and have your error handling code abstracted into one place, leaving your route handlers nice and tidy? The good news is that you can, by creating your own error handler middleware.

In this article you’ll learn how to create an error handler middleware function which behaves in a similar way to Express’ default error handler, but sends a JSON response. Just the error handler that your API needs!

Jump links


Getting errors to error handler middleware

The Express documentation has examples of an error being thrown e.g. throw new Error('..'), however this only works well when all of your code is synchronous, which is almost never in Node.js. If you do throw error objects in your Express application, you will need to be very careful about wrapping things so that next() is always called and that the error object is passed to it.

There are workarounds for error handling with asynchronous code in Express – where Promise chains are used, or async/await – however the fact is that Express does not have proper support built in for asynchronous code.

Error handling in Express is a broad and complex topic, and I plan to write more about this in the future, but for the purpose of this article we’ll stick with the most reliable way to handle errors in Express: always explicitly call next() with an error object e.g.

app.get("/user", (request, response, next) => {
	const sort = request.query.sort;

	if (!sort) {
		const error = new error("'sort' parameter missing from query string.");

		return next(error);
	}

	// ...
});

Creating an error handler

You can create and apply multiple error handler middleware in your application e.g. one error handler for validation errors, another error handler for database errors, however we’re going to create a generic error handler for our API. This generic error handler will send a JSON formatted response, and we’ll be applying the best practices that are detailed in the official Express Error handling guide. If you want, you’ll then be able to build on this generic error handler to create more specific error handlers.

Ok, let’s get stuck in!

Error handler concerns

Here are the things we’re going to take care of with our error handler middleware:

  • Log an error message to standard error (stderr) – in all environments e.g. development, production.
  • Delegate to the default Express error handler if headers have already been sent – The default error handler handles closing the connection and failing the request if you call next() with an error after you’ve started writing the response, so it’s important to delegate to the default error handler if headers have already been sent (source).
  • Extract an error HTTP status code – from an Error object or the Express response object.
  • Extract an error message – from an Error object, in all environments except production so that we don’t leak details about our application or the servers it runs on. In production the response body will be empty and the HTTP status code will be what clients use to determine the type of error that has occurred.
  • Send the HTTP status code and the error message as a response – the body will be formatted as JSON and we’ll send a Content-Type: application/json header.
  • Ensure remaining middleware is run – we might end up adding middleware after our error handler middleware in future e.g. to send request timing metrics to another server, so it’s important that our error handler middleware calls next(), otherwise we could end up in debugging hell in the future.

Error handler middleware function

In Express, error handling middleware are middleware functions that accept four arguments: (error, request, response, next). That first error argument is typically an Error object which the middleware will then handle.

As we saw above, there are quite a few concerns that our error handler needs to cover, so let’s first take a look at the error handler middleware function. Afterwards we’ll dig into the helper functions which it calls.

// src/middleware/error-handler.js

const NODE_ENVIRONMENT = process.env.NODE_ENV || "development";

/**
 * Generic Express error handler middleware.
 *
 * @param {Error} error - An Error object.
 * @param {Object} request - Express request object
 * @param {Object} response - Express response object
 * @param {Function} next - Express `next()` function
 */
function errorHandlerMiddleware(error, request, response, next) {
	const errorMessage = getErrorMessage(error);

	logErrorMessage(errorMessage);

	/**
	 * If response headers have already been sent,
	 * delegate to the default Express error handler.
	 */
	if (response.headersSent) {
		return next(error);
	}

	const errorResponse = {
		statusCode: getHttpStatusCode({ error, response }),
		body: undefined
	};

	/**
	 * Error messages and error stacks often reveal details
	 * about the internals of your application, potentially
	 * making it vulnerable to attack, so these parts of an
	 * Error object should never be sent in a response when
	 * your application is running in production.
	 */
	if (NODE_ENVIRONMENT !== "production") {
		errorResponse.body = errorMessage;
	}

	/**
	 * Set the response status code.
	 */
	response.status(errorResponse.statusCode);

	/**
	 * Send an appropriately formatted response.
	 *
	 * The Express `res.format()` method automatically
	 * sets `Content-Type` and `Vary: Accept` response headers.
	 *
	 * @see https://expressjs.com/en/api.html#res.format
	 *
	 * This method performs content negotation.
	 *
	 * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation
	 */
	response.format({
		//
		// Callback to run when `Accept` header contains either
		// `application/json` or `*/*`, or if it isn't set at all.
		//
		"application/json": () => {
			/**
			 * Set a JSON formatted response body.
			 * Response header: `Content-Type: `application/json`
			 */
			response.json({ message: errorResponse.body });
		},
		/**
		 * Callback to run when none of the others are matched.
		 */
		default: () => {
			/**
			 * Set a plain text response body.
			 * Response header: `Content-Type: text/plain`
			 */
			response.type("text/plain").send(errorResponse.body);
		},
	});

	/**
	 * Ensure any remaining middleware are run.
	 */
	next();
}

module.exports = errorHandlerMiddleware;

Error handler helper functions

There are three helper functions which are called by our error handler middleware function above:

  • getErrorMessage()
  • logErrorMessage()
  • getHttpStatusCode()

The benefit of creating these individual helper functions is that in future if we decide to create more specific error handling middleware e.g. to handle validation errors, we can use these helper functions as the basis for that new middleware.

Each of these helper functions are quite short, but they contain some important logic:

// src/middleware/error-handler.js

/**
 * Extract an error stack or error message from an Error object.
 *
 * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error
 *
 * @param {Error} error
 * @return {string} - String representation of the error object.
 */
function getErrorMessage(error) {
	/**
	 * If it exists, prefer the error stack as it usually
	 * contains the most detail about an error:
	 * an error message and a function call stack.
	 */
	if (error.stack) {
		return error.stack;
	}

	if (typeof error.toString === "function") {
		return error.toString();
	}

	return "";
}

/**
 * Log an error message to stderr.
 *
 * @see https://nodejs.org/dist/latest-v14.x/docs/api/console.html#console_console_error_data_args
 *
 * @param {string} error
 */
function logErrorMessage(error) {
	console.error(error);
}

/**
 * Determines if an HTTP status code falls in the 4xx or 5xx error ranges.
 *
 * @param {number} statusCode - HTTP status code
 * @return {boolean}
 */
function isErrorStatusCode(statusCode) {
	return statusCode >= 400 && statusCode < 600;
}

/**
 * Look for an error HTTP status code (in order of preference):
 *
 * - Error object (`status` or `statusCode`)
 * - Express response object (`statusCode`)
 *
 * Falls back to a 500 (Internal Server Error) HTTP status code.
 *
 * @param {Object} options
 * @param {Error} options.error
 * @param {Object} options.response - Express response object
 * @return {number} - HTTP status code
 */
function getHttpStatusCode({ error, response }) {
	/**
	 * Check if the error object specifies an HTTP
	 * status code which we can use.
	 */
	const statusCodeFromError = error.status || error.statusCode;
	if (isErrorStatusCode(statusCodeFromError)) {
		return statusCodeFromError;
	}

	/**
	 * The existing response `statusCode`. This is 200 (OK)
	 * by default in Express, but a route handler or
	 * middleware might already have set an error HTTP
	 * status code (4xx or 5xx).
	 */
	const statusCodeFromResponse = response.statusCode;
	if (isErrorStatusCode(statusCodeFromResponse)) {
		return statusCodeFromResponse;
	}

	/**
	 * Fall back to a generic error HTTP status code.
	 * 500 (Internal Server Error).
	 *
	 * @see https://httpstatuses.com/500
	 */
	return 500;
}

Now that we've created our error handler middleware, it's time to apply it in our application.

Applying the error handler middleware

Here is a complete example Express API application. It uses the http-errors library to add an HTTP status code to an error object and then passes it to the next() callback function. Express will then call our error handler middleware with the error object.

// src/server.js

const express = require("express");
const createHttpError = require("http-errors");

const errorHandlerMiddleware = require("./middleware/error-handler.js");

/**
 * In a real application this would run a query against a
 * database, but for this example it's always returning a
 * rejected `Promise` with an error message.
 */
function getUserData() {
	return Promise.reject(
		"An error occurred while attempting to run the database query."
	);
}

/**
 * Express configuration and routes
 */

const PORT = 3000;
const app = express();

/**
 * This route demonstrates:
 *
 * - Catching a (faked) database error (see `getUserData()` function above).
 * - Using the `http-errors` library to extend the error object with
 *   an HTTP status code.
 * - Passing the error object to the `next()` callback so our generic
 *   error handler can take care of it.
 */
app.get("/user", (request, response, next) => {
	getUserData()
		.then(userData => response.json(userData))
		.catch(error => {
			/**
			 * 500 (Internal Server Error) - Something has gone wrong in your application.
			 */
			const httpError = createHttpError(500, error);

			next(httpError);
		});
});

/**
 * Any error handler middleware must be added AFTER you define your routes.
 */
app.use(errorHandlerMiddleware);

app.listen(PORT, () =>
	console.log(`Example app listening at http://localhost:${PORT}`)
);

You can learn how to use the http-errors library in my article on 'How to send consistent error responses from your Express API'.

Example error response

Here's an example GET request with cURL to our /user endpoint, with the corresponding error response generated by our error handler middleware (in development):

$ curl -v http://localhost:3000/user

> GET /user HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.68.0
> Accept: */*
> 
< HTTP/1.1 500 Internal Server Error
< X-Powered-By: Express
< Vary: Accept
< Content-Type: application/json; charset=utf-8
< Content-Length: 279
< Connection: keep-alive
< 

{"message":"InternalServerError: An error occurred while attempting to run the database query.\n    at /dev/example/src/server.js:262:22\n    at processTicksAndRejections (internal/process/task_queues.js:97:5)"}

Next steps

You might have noticed that we're not sending a response body in production. This is due to the fact that sending the error object's message or call stack would leak details about our application, making it vulnerable to potential attackers. As we've created a generic error handler middleware here, the best that we can do is send back a suitable error HTTP status code in production.

If you know the types of the errors that your error handler middleware will be receiving (which you can check for example with error instanceof ErrorClass), you could define some production safe error messages which correspond with those error types. These production safe error messages could then be sent in the response body, providing more useful context about the error which has occurred. Give it a try!