Mar 09, 2023

Build a Custom Error-Handling Middleware for ExpressJS

Build a Custom Error-Handling Middleware for ExpressJS

In this article, you’ll learn how to overwrite the default error-handling middleware by creating your own middleware function that will centralize the errors thrown by the application, and execute the appropriate logic according to context.

The built-in error-handling middleware

The Express framework comes with a built-in error-handling middleware responsible for catching any runtime errors the application might encounter, thus preventing it from crashing.

By default, it will log the error in the console and respond to the client with an HTTP 500 Internal Server Error containing the error message as well as the stack trace (when not in production) in HTML format.

{
  status: 500,
  statusText: 'Internal Server Error',
  data: '<!DOCTYPE html>\n' + '<html lang="en">\n' + '<head>\n' + '<meta
charset="utf-8">\n' + '<title>Error</title>\n' + '</head>\n' + '<body>\n' +
'<pre>Error: BROKEN<br> &nbsp; &nbsp;at /Users/rludosanu/Projects/test/
test.js:5:9<br> &nbsp; &nbsp;at Layer.handle [as handle_request] (/Users/
rludosanu/Projects/test/node_modules/express/lib/router/layer.js:95:5)<br>
&nbsp; &nbsp;at next (/Users/rludosanu/Projects/test/node_modules/express/
lib/router/route.js:144:13)<br> &nbsp; &nbsp;at Route.dispatch (/Users/
rludosanu/Projects/test/node_modules/express/lib/router/route.js:114:3)<br>
&nbsp; &nbsp;at Layer.handle [as handle_request] (/Users/rludosanu/
Projects/test/node_modules/express/lib/router/layer.js:95:5)<br> &nbsp;
&nbsp;at /Users/rludosanu/Projects/test/node_modules/express/lib/router/
index.js:284:15<br> &nbsp; &nbsp;at Function.process_params (/Users/
rludosanu/Projects/test/node_modules/express/lib/router/
index.js:346:12)<br> &nbsp; &nbsp;at next (/Users/rludosanu/Projects/test/
node_modules/express/lib/router/index.js:280:10)<br> &nbsp; &nbsp;at
expressInit (/Users/rludosanu/Projects/test/node_modules/express/lib/
middleware/init.js:40:5)<br> &nbsp; &nbsp;at Layer.handle [as
handle_request] (/Users/rludosanu/Projects/test/node_modules/express/lib/
router/layer.js:95:5)</pre>\n' + '</body>\n' + '</html>\n'
}

Although useful when the application is in an early stage of development, this default handler shows its limits when it comes to real world APIs that must be capable of handling multiple types of errors in different ways, like for example responding to the client with different status codes or writing these errors into log files.

A basic error-handling middleware

Creating the middleware

In Express, an error-handling middleware function is just like any other middleware, except that it takes 4 arguments instead of 3:

// File: error-handler.js

function errorHandler(err, req, res, next) {
  console.error(err);
  res.sendStatus(500);
}

module.exports = errorHandler;

Mounting the middleware

This middleware must be mounted to the application through the app.use() method at the very bottom of the call stack, after all the other declarations:

// File: app.js

const express = require('express');
const app = express();

const errorHandler = require('./error-handler');

app.get('/health', (req, res) => { /* ... */ });

app.post('/login', (req, res) => { /* ... */ });

app.post('/signup', (req, res) => { /* ... */ });

app.use(errorHandler);

app.listen(3000);

Invoking the middleware

To forward errors to this middleware, all we have to do is invoke the next() parameter of the route controller, which will pass the request down to the next stack component:

// File: app.js

const express = require('express');
const app = express();

const errorHandler = require('./error-handler');

app.post('/login', (req, res, next) => {
  next(new Error('FAILED'));
});

app.use(errorHandler);

app.listen(3000);

Testing the middleware

You can try it yourself by running this application in a new terminal window:

$ node app.js

And by sending the following request to the POST /login route using cURL:

$ curl -v -X POST 127.0.0.1:3000/login

Throwing custom errors

Creating a new Error class

To be able to throw custom errors, we’re going to create a new error class named CustomError that inherits from the built-in Error class, where the constructor will take as argument a code constant representing the type of custom error raised by the application. For example INVALID_EMAIL for an invalid email address or USER_NOT_FOUND for an email/password pair that doesn’t exist in the database.

// File: custom-error.js

class CustomError extends Error {
  constructor(code) {
    super();
    this.code = code;
  }
}

module.exports = CustomError;

Defining custom error codes

We're now going to create an object that maps custom error codes to valid HTTP statuses and human-readable messages.

// File: error-codes.js

module.exports = {
  INVALID_EMAIL_OR_PASSWORD: {
    statusCode: 400, // Bad Request
    message: 'Invalid email address or password',
  },
  USER_NOT_FOUND: {
    statusCode: 404, // Not Found
    message: 'User not found',
  },
  INTERNAL_ERROR: {
    statusCode: 500, // Internal Server Error
    message: 'Internal Server Error',
  },
};

Improving the error-handling middleware

Now that we have a CustomError class and a list of custom errors, we can improve our error-handling middleware so that it:

  • Parses the error code contained in the err object.
  • Matches the error code against the list of custom errors.
  • Sends a response to the client with the appropriate status code and error message, or a standard HTTP 500 Internal Server Error.
// File: error-handler.js

const errorCodes = require('./error-codes');

function errorHandler(err, req, res, next) {
  const code = (err && err.code) || null;
  const error = errorCodes[code] || errorCodes['INTERNAL_ERROR'];

  return res
    .status(error.statusCode)
    .json({ message: error.message });
};

module.exports = errorHandler;

A complete example

To illustrate this, we're going to create a simple application that implements a POST /login route allowing users to authenticate with an email address and a password.

The application

When a request reaches the POST /login route, its payload is converted to a JavaScript object using the built-in express.json() middleware, and forwarded to the loginController() function.

// File: app.js

const express = require('express');
const app = express();

const loginController = require('./login-controller');
const errorHandler = require('./error-handler.js');

app.post('/login', express.json(), loginController);

app.use(errorHandler);

app.listen(3000);

The controller

The loginController() calls the loginService() with the parsed data contained in the req.body property. If the authentication is successful, it responds to the client with an HTTP 200 containing an access token string. Otherwise, it forwards any error raised to the error-handling middleware using the next() function.

// File: login-controller.js

const loginService = require('./login-service');

function loginController(req, res, next) {
  try {
    const token = loginService(req.body);
    res.json({ token });
  } catch(error) {
    next(error);
  }
}

module.exports = loginController;

The service

The loginService() verifies the data sent by the loginController() and throws a CustomError if the email or the password are invalid, or if they don't match the expected values. Otherwise, it return a string representing an access token.

// File: login-service.js

const CustomError = require('./custom-error');

function loginService(data) {
  const { email, password } = data;
  const token = 'access_token';

  if (!email || !password) {
    throw new CustomError('INVALID_EMAIL_OR_PASSWORD');
  }

  if (email !== 'user@test.com' || password !== 'helloworld') {
    throw new CustomError('USER_NOT_FOUND');
  }

  return token;
}

module.exports = loginService;

Testing the application

You can now run this application in a new terminal window:

$ node app.js

And verify that we get an HTTP 200 containing an access token when the email and the password match:

$ curl -v -X POST \
-H 'Content-Type: application/json' \
-d '{"email":"user@test.com","password":"helloworld"}' \
127.0.0.1:3000/login

That we can an HTTP 404 when the email doesn't match:

$ curl -v -X POST \
-H 'Content-Type: application/json' \
-d '{"email":"unknown@test.com","password":"helloworld"}' \
127.0.0.1:3000/login

That we can an HTTP 400 when the email is missing:

$ curl -v -X POST \
-H 'Content-Type: application/json' \
-d '{"email":"","password":"helloworld"}' \
127.0.0.1:3000/login

Related posts