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.

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
- In your
src/middleware/directory, create a new file namedasyncHandler.js. - 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 functionasyncHandlerthat accepts one argument,fn, which will be our asynchronous route handler.... (req, res, next) => { ... }:asyncHandlerimmediately 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 handlerfn.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 fromfnis 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'snextfunction is designed to accept an error as its first argument to trigger the error-handling middleware, we can simply passnextdirectly. 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
- Open your primary routes file (e.g.,
src/routes/item.routes.js). - Import the
asyncHandlerat the top of the file. - Wrap each of your
asyncroute handler functions withasyncHandler().
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.
- Ensure your Mongoose schema has a required field (e.g.,
name: { type: String, required: true }). - Start your server with
npm run dev. - Open Postman and create a
POSTrequest to create a new item. - In the request body, deliberately omit the required field. For example, send an empty JSON object
{}. - 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
asyncExpress route handlers (rejected promises) are not caught by Express automatically and will crash your server. - The
asyncHandlerutility is a higher-order function that provides a reusabletry...catchmechanism 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.