Sling Academy
Home/Node.js/Node & Express: Handling Errors in Async Middleware

Node & Express: Handling Errors in Async Middleware

Last updated: December 28, 2023

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.

Next Article: How to Get a URL Parameter in Express

Previous Article: How to Render HTML Views in Express.js

Series: Node.js & Express Tutorials

Node.js

You May Also Like

  • NestJS: How to create cursor-based pagination (2 examples)
  • Cursor-Based Pagination in SequelizeJS: Practical Examples
  • MongooseJS: Cursor-Based Pagination Examples
  • Node.js: How to get location from IP address (3 approaches)
  • SequelizeJS: How to reset auto-increment ID after deleting records
  • SequelizeJS: Grouping Results by Multiple Columns
  • NestJS: Using Faker.js to populate database (for testing)
  • NodeJS: Search and download images by keyword from Unsplash API
  • NestJS: Generate N random users using Faker.js
  • Sequelize Upsert: How to insert or update a record in one query
  • NodeJS: Declaring types when using dotenv with TypeScript
  • Using ExpressJS and Multer with TypeScript
  • NodeJS: Link to static assets (JS, CSS) in Pug templates
  • NodeJS: How to use mixins in Pug templates
  • NodeJS: Displaying images and links in Pug templates
  • ExpressJS + Pug: How to use loops to render array data
  • ExpressJS: Using MORGAN to Log HTTP Requests
  • NodeJS: Using express-fileupload to simply upload files
  • ExpressJS: How to render JSON in Pug templates