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:

  1. Catch the error.

  2. 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)
    
  3. Respond to the client with an HTTP 500 Internal Server Error containing 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> &nbsp; &nbsp;at /Users/razvan/learnbackend/server/index.js:8:15<br> &nbsp; &nbsp;at Layer.handle [as handle_request] (/Users/razvan/learnbackend/server/node_modules/express/lib/router/layer.js:95:5)<br> &nbsp; &nbsp;at next (/Users/razvan/learnbackend/server/node_modules/express/lib/router/route.js:144:13)<br> &nbsp; &nbsp;at Route.dispatch (/Users/razvan/learnbackend/server/node_modules/express/lib/router/route.js:114:3)<br> &nbsp; &nbsp;at Layer.handle [as handle_request] (/Users/razvan/learnbackend/server/node_modules/express/lib/router/layer.js:95:5)<br> &nbsp; &nbsp;at /Users/razvan/learnbackend/server/node_modules/express/lib/router/index.js:284:15<br> &nbsp; &nbsp;at Function.process_params (/Users/razvan/learnbackend/server/node_modules/express/lib/router/index.js:346:12)<br> &nbsp; &nbsp;at next (/Users/razvan/learnbackend/server/node_modules/express/lib/router/index.js:280:10)<br> &nbsp; &nbsp;at expressInit (/Users/razvan/learnbackend/server/node_modules/express/lib/middleware/init.js:40:5)<br> &nbsp; &nbsp;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_ENV environment variable to production.

    $ 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:

  • err represents the error object thrown or forwarded using the next() 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...catch block and forwarded to the error-handling middleware using the next() 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
Handle Runtime Errors in Express.js | Backend Brewery