When I first started building APIs with Express.js, every async controller looked the same. I would write a try block, perform some database operations, and then write a catch block that called next(error).
It worked, so I copied the same pattern into every controller. One controller became ten. Ten became fifty. Eventually, I realized that half of my controller code wasn't actually business logic, it was just repetitive error handling.
That's when I discovered the Async Handler pattern.
The Problem
A typical Express controller often looks like this:
export const getUser = async (req, res, next) => {
try {
const user = await User.findById(req.params.id);
if (!user) {
throw new Error("User not found");
}
res.json(user);
} catch (error) {
next(error);
}
};
There's nothing wrong with this code.
The problem is that every async controller ends up looking exactly the same.
Every file contains:
try, catch and next(error)
over and over again. Besides being repetitive, it's also easy to forget. Miss one try-catch block, and Express won't automatically catch errors thrown inside async functions.
What Is an Async Handler?
An async handler is a small wrapper function that automatically catches errors from async controllers. Instead of every controller handling its own errors, the wrapper does it for you.
A Simple Analogy
Imagine an office where every employee has to stop working whenever someone rings the front door. Besides doing their own job, they also have to greet every visitor. This quickly becomes repetitive and inefficient. Instead, the company hires a receptionist to handle every visitor. Now the employees can focus on their actual work while the receptionist takes care of the door. An async handler works the same way. Controllers focus on handling requests, while the async handler catches errors and passes them to Express's error handler.
Without an Async Handler
export const createUser = async (req, res, next) => {
try {
const user = await User.create(req.body);
res.status(201).json(user);
} catch (error) {
next(error);
}
};
It looks fine. Now imagine writing this in 40 different controllers. Most of the code ends up being identical.
With an Async Handler
First, create the wrapper.
export const asyncHandler = (fn) => {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
};
Now the controller becomes:
export const createUser = asyncHandler(async (req, res) => {
const user = await User.create(req.body);
res.status(201).json(user);
});
Notice how the controller only contains its actual responsibility. There is no try-catch. If anything throws an error, the async handler catches it automatically and forwards it to Express.
How It Works
The wrapper accepts your controller function.
asyncHandler(fn)
When Express executes it:
Your controller runs.
If everything succeeds, Express sends the response.
If an error is thrown or a Promise rejects,
.catch(next)is called.Express forwards the error to your global error middleware.
Every controller gets consistent error handling without repeating code.
Common Mistakes to Avoid
1. Wrapping synchronous controllers
Async handlers are designed for asynchronous functions. Normal synchronous routes usually don't need them.
2. Using try-catch inside every wrapped controller
If you're already using an async handler, most controllers don't need their own try-catch blocks. Only use one when you want to recover from a specific error.
3. Forgetting a global error middleware
An async handler forwards errors; it doesn't send responses. Without a global error handler, those errors still won't be returned properly. So, create the global error middleware first.
4. Mixing business logic with error formatting
Controllers should call services. The global error middleware should decide how errors are returned. Keep those responsibilities separate.
Conclusion
An async handler doesn't make your application faster. It makes your code cleaner.
Instead of writing the same try-catch block in every controller, you write the wrapper once and reuse it everywhere. The result is smaller controllers, consistent error handling, and code that's much easier to maintain.
The lesson is simple:
Don't repeat error handling in every controller. Write it once, wrap your controllers, and let Express handle the rest.
Top comments (0)