Mar 09, 2023
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> at /Users/rludosanu/Projects/test/
test.js:5:9<br> at Layer.handle [as handle_request] (/Users/
rludosanu/Projects/test/node_modules/express/lib/router/layer.js:95:5)<br>
at next (/Users/rludosanu/Projects/test/node_modules/express/
lib/router/route.js:144:13)<br> at Route.dispatch (/Users/
rludosanu/Projects/test/node_modules/express/lib/router/route.js:114:3)<br>
at Layer.handle [as handle_request] (/Users/rludosanu/
Projects/test/node_modules/express/lib/router/layer.js:95:5)<br>
at /Users/rludosanu/Projects/test/node_modules/express/lib/router/
index.js:284:15<br> at Function.process_params (/Users/
rludosanu/Projects/test/node_modules/express/lib/router/
index.js:346:12)<br> at next (/Users/rludosanu/Projects/test/
node_modules/express/lib/router/index.js:280:10)<br> at
expressInit (/Users/rludosanu/Projects/test/node_modules/express/lib/
middleware/init.js:40:5)<br> 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