Express has long been a staple in the Node.js ecosystem, powering thousands of APIs and web applications. However, until recently, handling asynchronous route logic in Express has always felt a bit clunky β particularly when it comes to error handling in async functions.
But good news: Express 5 finally brings first-class support for async/await with native promise error handling! That means you can now write clean, modern async route handlers without wrappers like asyncHandler().
In this post, weβll walk through:
- π€― The problem in Express 4
- β The solution in Express 5
- π οΈ Migration tips
- π§ͺ Real-world example
π€― The Problem in Express 4
In Express 4, writing async handlers directly like this:
app.get('/users', async (req, res) => {
const users = await getUsersFromDb(); // what if this throws?
res.json(users);
});
would result in an unhandled promise rejection if getUsersFromDb() throws or rejects.
Why? Because Express 4 does not understand that async functions return promises. It doesnβt await them or catch their rejections automatically.
Workaround: Using asyncHandler()
To deal with this, most devs used a helper like:
const asyncHandler = fn => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
Then youβd wrap every route:
app.get('/users', asyncHandler(async (req, res) => {
const users = await getUsersFromDb();
res.json(users);
}));
Clunky, right?
β
The Fix in Express 5
In Express 5, this boilerplate is no longer needed. You can now write:
app.get('/users', async (req, res) => {
const users = await getUsersFromDb(); // if this throws, Express 5 will catch it
res.json(users);
});
π Express 5 automatically awaits promises and forwards errors to the error handler!
π§ͺ Real-World Example
Letβs say you have this:
app.get('/profile', async (req, res) => {
const user = await findUserById(req.query.id);
if (!user) throw new Error('User not found');
res.json(user);
});
If anything throws (like a DB error), Express 5 will automatically send it to your error middleware:
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ message: err.message });
});
No more asyncHandler. No more accidental unhandled rejections.
π οΈ Migration Tips
If you're upgrading from Express 4 to 5:
- Remove asyncHandler() wrappers β just use async route functions directly.
- Double-check your error middleware β Express 5 continues to use the (err, req, res, next) signature.
- Update your dependencies β install the latest Express version:
npm install express@next
β οΈ At the time of writing, Express 5 is in beta (express@5.0.0-beta.x). Keep an eye on the official changelog for production readiness.
app.use(async (req, res, next) => {
const isValid = await validateToken(req.headers.authorization);
if (!isValid) throw new Error('Unauthorized');
next();
});
Again, no try/catch needed β Express 5 has your back.
π§Ύ Summary
β
Express 5 brings native async/await support
β
No need for async wrappers
β
Clean, modern error handling
β
Works in both routes and middleware
β
Big win for developer experience!
π References
- Express.js Error Handling Guide
- Express 5.x API Reference
- What's New in Express.js v5.0
- Express Error Handling Patterns
- Handling Errors with Async/Await in Express
Add Basic Error Handling to an Express 5 App
π Conclusion
Express 5 may not seem flashy at first glance, but this feature alone dramatically simplifies writing clean, maintainable APIs with modern JavaScript. If youβve been juggling asyncHandler utilities in Express 4, it's time to celebrate and simplify.
Happy coding!
βοΈ Follow me on Dev.to or GitHub for more Node.js tips, architecture guides, and backend tutorials.
Top comments (0)