Handle Runtime Errors in Express
16 min read·Jan 1, 2025
In Express, runtime errors are handled by a special component called an error-handling middleware.
Its role is to catch these errors to prevent the application from crashing and to respond to clients with an error message indicating that something went wrong when trying to process the request.
The default error-handling middleware
By default, when an error is raised outside of a try...catch block, the built-in error-handling middleware will:
-
Catch the error.
-
Log it in the application's console:
RangeError: Division by zero at /Users/razvan/learnbackend/server/index.js:8:15 at Layer.handle [as handle_request] (/Users/razvan/learnbackend/server/node_modules/express/lib/router/layer.js:95:5) at next (/Users/razvan/learnbackend/server/node_modules/express/lib/router/route.js:144:13) at Route.dispatch (/Users/razvan/learnbackend/server/node_modules/express/lib/router/route.js:114:3) at Layer.handle [as handle_request] (/Users/razvan/learnbackend/server/node_modules/express/lib/router/layer.js:95:5) at /Users/razvan/learnbackend/server/node_modules/express/lib/router/index.js:284:15 at Function.process_params (/Users/razvan/learnbackend/server/node_modules/express/lib/router/index.js:346:12) at next (/Users/razvan/learnbackend/server/node_modules/express/lib/router/index.js:280:10) at expressInit (/Users/razvan/learnbackend/server/node_modules/express/lib/middleware/init.js:40:5) at Layer.handle [as handle_request] (/Users/razvan/learnbackend/server/node_modules/express/lib/router/layer.js:95:5) -
Respond to the client with an HTTP
500 Internal Server Errorcontaining the error message as well as the stack trace in HTML format:<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Error</title> </head> <body> <pre>RangeError: Division by zero<br> at /Users/razvan/learnbackend/server/index.js:8:15<br> at Layer.handle [as handle_request] (/Users/razvan/learnbackend/server/node_modules/express/lib/router/layer.js:95:5)<br> at next (/Users/razvan/learnbackend/server/node_modules/express/lib/router/route.js:144:13)<br> at Route.dispatch (/Users/razvan/learnbackend/server/node_modules/express/lib/router/route.js:114:3)<br> at Layer.handle [as handle_request] (/Users/razvan/learnbackend/server/node_modules/express/lib/router/layer.js:95:5)<br> at /Users/razvan/learnbackend/server/node_modules/express/lib/router/index.js:284:15<br> at Function.process_params (/Users/razvan/learnbackend/server/node_modules/express/lib/router/index.js:346:12)<br> at next (/Users/razvan/learnbackend/server/node_modules/express/lib/router/index.js:280:10)<br> at expressInit (/Users/razvan/learnbackend/server/node_modules/express/lib/middleware/init.js:40:5)<br> at Layer.handle [as handle_request] (/Users/razvan/learnbackend/server/node_modules/express/lib/router/layer.js:95:5)</pre> </body> </html>Note: To prevent the stack trace from being included, you can start the server in production mode by setting the
NODE_ENVenvironment variable toproduction.$ NODE_ENV=production node server.js
Synchronous vs. asynchronous errors
When an error is raised and uncaught in synchronous code, no extra work is required as the default error-handling middleware will automatically catch it and handle it as shown in the section above.
On the other hand, when an error occurs in asynchronous code, it must be manually caught using a try...catch block and forwarded to the error-handling middleware using the next() handler, as it will otherwise be omitted by Express since it is not part of the synchronous handler code.
Example: Synchronous errors
In this example, the error-handling middleware will automatically catch and handle the error thrown in the controller when a request reaches the server on the HTTP GET / route:
const express = require('express');
const server = express();
server.get('/', (req, res) => {
throw new Error('BROKEN');
});
server.listen(3000);
Example: Asynchronous errors
In this example, the error thrown in the HTTP GET / route's controller will be caught by the catch() function and manually forwarded to the error-handling middleware through a call to the next() handler:
const express = require('express');
const server = express();
server.get('/', (req, res, next) => {
setTimeout(() => {
try {
throw new Error('BROKEN');
} catch (error) {
next(error);
}
}, 100);
});
server.listen(3000);
Overwrite the error-handling middleware
Although useful when your application is in an early stage of development, the default error-handling middleware shows its limits when it comes to real world APIs that must be capable of handling multiple error types in different ways.
The error-handling middleware signature
In Express, an error-handling middleware function is just like any other middleware, except that it takes four arguments instead of three:
function(err, req, res, next) {
// ...
}
Where:
errrepresents the error object thrown or forwarded using thenext()handler by a component of the application.
Beyond executing internal logic, such as logging errors, it is responsible for terminating the request-response lifecycle by sending a response to the client containing the appropriate status code and error message, as the client's request would otherwise be left hanging.
Example
In this example, this function mimics the behavior of the default error-handling middleware by logging the error into the terminal and responding to the client with a HTTP 500 Internal Server Error:
function(err, req, res, next) {
console.error(err);
res.sendStatus(500);
}
Mount the error-handling middleware
Since Express works in a declarative manner (from top to bottom) the error-handling middleware must be mounted to the application instance at the very bottom of the call stack, after all the other components, such as middlewares, routes, and routers:
const express = require('express');
const server = express();
// ...
server.use((err, req, res, next) => {
// ...
});
server.listen(3000);
Example
In this example, the custom error-handling middleware will catch and handle the error thrown in the controller when a request reaches the server on the HTTP GET /error route:
const express = require('express');
const server = express();
server.get('/error', (req, res) => {
throw new Error('BROKEN');
});
server.use((err, req, res, next) => {
console.error(err);
res.sendStatus(500);
});
server.listen(3000);
When sending the following request to that endpoint using the curl command, the server responds with an HTTP 500 Internal Server Error:
$ curl 127.0.0.1:3000/error
HTTP/1.1 500 Internal Server Error
X-Powered-By: Express
Content-Type: text/plain; charset=utf-8
Content-Length: 21
ETag: W/"15-/6VXivhc2MKdLfIkLcUE47K6aH0"
Date: Mon, 04 Mar 2024 11:39:39 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Internal Server Error
And the server logs that error in the terminal using the console.error() method:
$ node server.js
Error: BROKEN
at /Users/razvan/scripts/express/server.js:6:9
at Layer.handle [as handle_request] (/Users/razvan/scripts/express/node_modules/express/lib/router/layer.js:95:5)
at next (/Users/razvan/scripts/express/node_modules/express/lib/router/route.js:144:13)
at Route.dispatch (/Users/razvan/scripts/express/node_modules/express/lib/router/route.js:114:3)
at Layer.handle [as handle_request] (/Users/razvan/scripts/express/node_modules/express/lib/router/layer.js:95:5)
at /Users/razvan/scripts/express/node_modules/express/lib/router/index.js:284:15
at Function.process_params (/Users/razvan/scripts/express/node_modules/express/lib/router/index.js:346:12)
at next (/Users/razvan/scripts/express/node_modules/express/lib/router/index.js:280:10)
at expressInit (/Users/razvan/scripts/express/node_modules/express/lib/middleware/init.js:40:5)
at Layer.handle [as handle_request] (/Users/razvan/scripts/express/node_modules/express/lib/router/layer.js:95:5)
Summary
Here's a summary of what you've learned in this lesson:
- An error-handling middleware is a component responsible for catching runtime errors and responding to the client with an error message.
- In synchronous code, Express.js will automatically catch and handle errors.
- In asynchronous code, errors must be manually caught in a
try...catchblock and forwarded to the error-handling middleware using thenext()handler. - The default error-handling middleware can be overwritten by declaring a new middleware at the bottom of the call stack through the
use()method of the application instance. - The custom error-handling middleware must have the following signature
function(err, req, res, next).
Enjoying the courses?
I've made these courses completely free so anyone can learn from them. If they've helped you and you'd like to actively support the work behind BackendBrewery, you can leave a tip:
Support BackendBrewery