Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 19

Handling Asynchronous Errors in Express.js

Recommended resources

Introduction

In the previous lesson, you built a centralized global error handler. It successfully catches errors you manually forward with next(error), like the one from your 404 handler, and turns them into clean JSON responses.

However, there's a critical gap in our error handling. Your route handlers that interact with the database are async functions. By default, if an error occurs inside an async function in Express (for example, a database query fails), the resulting "rejected promise" is not caught. This will cause your server to hang or crash—unacceptable for a production application.

In this lesson, you will implement a professional, reusable pattern to solve this. We'll create a simple utility function that wraps your async route handlers. This wrapper's sole job is to catch any rejected promises and correctly forward the error to your global error handler using next(err). This eliminates the need for repetitive try...catch blocks and makes your route handlers cleaner and more robust.


1. The Problem with Asynchronous Errors

The "naive" solution to this problem would be to wrap the code inside every single async route handler with a try...catch block.

// The repetitive, "naive" approach
router.post('/', async (req, res, next) => {
  try {
    const newItem = await Item.create(req.body);
    res.status(201).json({ success: true, data: newItem });
  } catch (err) {
    next(err); // Manually forward the error
  }
});

router.put('/:id', async (req, res, next) => {
  try {
    const updatedItem = await Item.findByIdAndUpdate(req.params.id, req.body, { new: true });
    res.status(200).json({ success: true, data: updatedItem });
  } catch (err) {
    next(err); // Manually forward the error... again
  }
});

While this works, it's not a good solution. It violates the DRY (Don't Repeat Yourself) principle and clutters your route handlers with boilerplate error-catching code, distracting from their primary business logic. We need a more elegant solution.


2. The Solution: An asyncHandler Wrapper

The professional pattern is to use a higher-order function—a function that takes another function as an argument—to handle this logic centrally. We'll create a utility called asyncHandler. You'll wrap your route handler logic with this utility, and it will automatically provide the try...catch behavior.

The following diagram illustrates this flow. Our asyncHandler is the component responsible for catching the error from the service call and forwarding it to the global error middleware you built previously.

Error Handling Flow with Global Middleware
This sequence diagram shows an error from an `async` service call being caught and forwarded via `next(err)` to the `GlobalErrorMiddleware`, which then formats the final JSON response.

Let's watch a video that perfectly demonstrates how to build this utility from the ground up. It explains the motivation and the technical details of the implementation.

#93 Error Handling in async function | Error Handling in Express | A Complete NODE JS Course

The following video from the procademy channel walks through the exact process of identifying the problem with async errors and developing the asyncErrorHandler solution.

Watch from 02:20 to 09:44. Pay close attention to these key moments: (02:20 - 03:35): The motivation: why we want to remove try...catch from our controllers. (03:35 - 06:50): The first attempt at an asyncErrorHandler function and the explanation of how a promise's .catch() method can handle rejections. (06:50 - 09:44): The crucial refinement. You'll see why the first attempt fails and how returning an anonymous function solves the problem by giving Express a function it can call at the correct time.


3. Implementing the asyncHandler Utility

As you saw in the video, the final implementation is surprisingly concise. Let's create the file and add the code.

Action: Create the asyncHandler.js File

  1. In your src/middleware/ directory, create a new file named asyncHandler.js.
  2. Add the following code to the file. This is the exact pattern used in professional Express applications.
// src/middleware/asyncHandler.js

const asyncHandler = (fn) => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

module.exports = asyncHandler;

Let's quickly break down this powerful line of code:

  • const asyncHandler = (fn) => ...: We define a function asyncHandler that accepts one argument, fn, which will be our asynchronous route handler.
  • ... (req, res, next) => { ... }: asyncHandler immediately returns a new anonymous function. This is the function that Express will actually execute when a request hits the route. It has the standard (req, res, next) signature.
  • Promise.resolve(fn(req, res, next)): Inside the returned function, we call our original route handler fn. Promise.resolve() wraps the execution, ensuring that its result is treated as a Promise.
  • .catch(next): This is the most critical part. If the promise from fn is rejected (i.e., an error is thrown), the .catch() method is triggered. The error object from the rejection is passed as the first argument to whatever function is inside .catch(). Since Express's next function is designed to accept an error as its first argument to trigger the error-handling middleware, we can simply pass next directly. This is a clean shorthand for .catch(err => next(err)).

4. Applying the asyncHandler to Your Routes

Now, let's put our new utility to use. You will refactor your routes file to wrap all asynchronous handlers.

Action: Update Your Routes File

  1. Open your primary routes file (e.g., src/routes/item.routes.js).
  2. Import the asyncHandler at the top of the file.
  3. Wrap each of your async route handler functions with asyncHandler().

Your code should transform from this:

// Before
const express = require('express');
const router = express.Router();
const Item = require('../models/item.model');

// GET all items
router.get('/', async (req, res) => {
  const items = await Item.find();
  res.status(200).json({ success: true, data: items });
});

// POST a new item
router.post('/', async (req, res) => {
  const newItem = await Item.create(req.body);
  res.status(201).json({ success: true, data: newItem });
});
// ... and so on for other routes

To this:

// After
const express = require('express');
const router = express.Router();
const Item = require('../models/item.model');
const asyncHandler = require('../middleware/asyncHandler'); // 1. Import

// GET all items
router.get('/', asyncHandler(async (req, res) => { // 2. Wrap the handler
  const items = await Item.find();
  res.status(200).json({ success: true, data: items });
}));

// POST a new item
router.post('/', asyncHandler(async (req, res) => { // 2. Wrap the handler
  // Deliberately remove any try/catch blocks you may have had
  const newItem = await Item.create(req.body);
  res.status(201).json({ success: true, data: newItem });
}));

// ... apply to all other async route handlers (PUT, DELETE, GET by ID)

Make sure to apply this to all your asynchronous route handlers. With this change, any database error or other exception thrown in your route logic will be automatically caught and sent to your global errorHandler.


5. Testing the Full Error Pipeline

Let's verify that the entire chain works. We will trigger a Mongoose validation error and confirm it's handled gracefully.

  1. Ensure your Mongoose schema has a required field (e.g., name: { type: String, required: true }).
  2. Start your server with npm run dev.
  3. Open Postman and create a POST request to create a new item.
  4. In the request body, deliberately omit the required field. For example, send an empty JSON object {}.
  5. Send the request.

Expected Outcome:

Instead of your server crashing, you should receive a clean JSON error response from your global error handler. Because Mongoose validation errors don't have a .status property by default, your errorHandler will likely default to a 500 Internal Server Error, which is correct for now. The response body should look something like this:

{
    "success": false,
    "status": 500,
    "message": "Item validation failed: name: Path `name` is required."
}

This proves your asyncHandler caught the promise rejection from Item.create() and passed the Mongoose error object to next(), which then triggered your errorHandler to send the final response.


Conclusion

You have now made your API significantly more robust. By implementing the asyncHandler pattern, you have ensured that errors in your asynchronous database operations are handled gracefully and consistently without cluttering your route handlers.

Key Takeaways:

  • Errors in async Express route handlers (rejected promises) are not caught by Express automatically and will crash your server.
  • The asyncHandler utility is a higher-order function that provides a reusable try...catch mechanism for your routes.
  • It works by executing your route handler and using .catch(next) to forward any errors to your global error handling middleware.
  • This pattern keeps your route handlers clean, focused on business logic, and follows the DRY principle.

Next Up:

Your API is now functional and robust. The next step is to make it discoverable and easy to use for developers. In the following lesson, you will install and configure Swagger to generate live, interactive API documentation directly from your code.