Node & Express: Handling Errors in Async Middleware

Updated: December 28, 2023 By: Guest Contributor Post a comment

Overview

Error handling in Node.js and Express applications is a crucial aspect of creating robust and reliable software. Especially when dealing with asynchronous operations, which are common in Node.js, it is important to ensure that errors are caught and handled properly. In this article, we will explore how to handle errors in asynchronous middleware functions in an Express.js application.

Since Express does not catch exceptions thrown in asynchronous functions by default, developers need to implement a proper error handling strategy to avoid server crashes and unresponsive requests.

Basic Async Error Handling

Let’s start with the basic approach to handling errors in asynchronous middleware using the traditional try..catch blocks:

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

app.get('/data', async (req, res, next) => {
  try {
    const data = await fetchData();
    res.json(data);
  } catch (error) {
    next(error);
  }
});

app.use((error, req, res, next) => {
  res.status(500).json({ error: 'An error occurred' });
});

In the above example, we define an asynchronous route handler. If an error occurs during the fetching of data, it is caught in the try..catch block and passed to the next middleware, which is the error-handling middleware that sends a response back to the client.

Express Async Errors Package

You can simplify asynchronous error handling by using the express-async-errors package, which monkey patches Express to automatically catch exceptions from async functions and pass them to your error handlers:

require('express-async-errors');
const express = require('express');
const app = express();

// ... rest of your route definitions

app.use((error, req, res, next) => {
  res.status(500).json({ error: 'Internal Server Error' });
});

With the express-async-errors package included in your project, there is no need to wrap your route handlers in try..catch blocks, as the package takes care of forwarding the errors to your error-handling middleware.

Advanced Error Handling Patterns

For more sophisticated error handling, you might want to define a custom error class and use a centralized error-handling middleware:

class AsyncError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.statusCode = statusCode;
    this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
    this.isOperational = true;

    Error.captureStackTrace(this, this.constructor);
  }
}

app.get('/data', async (req, res, next) => {
  const data = await fetchData();
  if (!data) {
    throw new AsyncError('Data not found', 404);
  }
  res.json(data);
});

// The centralized error handler
app.use((error, req, res, next) => {
  if (error.isOperational) {
    res.status(error.statusCode).json({
      status: error.status,
      message: error.message,
    });
  } else {
    console.error('ERROR 💥', error);
    res.status(500).json({
      status: 'error',
      message: 'Something went very wrong!',
    });
  }
});

This pattern provides more control and can help separate operational errors (which we expect in the normal functioning of the application) from programming errors, which are bugs that should be logged and fixed.

Using Promises

Alternatively, you can use promises to handle errors. Every async function returns a promise, so you can attach a .catch() to the route handler to pass errors to the centralized error handler:

app.get('/data', (req, res, next) => {
  fetchData().then(data => res.json(data)).catch(next);
});

Note that in this case, you do not use an async function but instead return the result of fetchData() with a promise chain.

Conclusion

Error handling in asynchronous middleware in Node.js and Express is a critical skill for developers to ensure the stability and reliability of their applications. By using try..catch, the express-async-errors package, custom error classes, and promise chains, you can create a strategy that is well-suited for your application’s needs.

Remember that proper error logging and monitoring is also essential, as it helps detect and diagnose issues quickly, leading to a better user experience and more maintainable codebase.