How to handle async/await in Express.js route handlers?
Async/Await in Express.js
Express was designed before async/await existed. By default, Express does not catch unhandled Promise rejections from async route handlers β they crash your app silently or cause unhandled rejection warnings.
The Problem
// β BROKEN β unhandled Promise rejection!
app.get('/users', async (req, res) => {
const users = await User.findAll(); // throws? Express doesn't catch it!
res.json(users);
});If User.findAll() rejects, the error is not passed to Express's error handler.
Solution 1: try/catch + next(err)
// β
Correct β but verbose
app.get('/users', async (req, res, next) => {
try {
const users = await User.findAll();
res.json(users);
} catch (err) {
next(err); // forward to error handler
}
});This works but writing try/catch in every handler is tedious.
Solution 2: asyncHandler Wrapper
// utils/asyncHandler.js
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
module.exports = asyncHandler;
// Clean route handlers!
const asyncHandler = require('../utils/asyncHandler');
app.get('/users', asyncHandler(async (req, res) => {
const users = await User.findAll();
res.json(users);
}));
app.post('/users', asyncHandler(async (req, res) => {
const user = await User.create(req.body);
res.status(201).json(user);
}));Solution 3: express-async-errors (Monkey-Patch)
npm install express-async-errors// Import ONCE at the top of app.js
require('express-async-errors');
// Now async errors are forwarded to next() automatically!
app.get('/users', async (req, res) => {
const users = await User.findAll(); // if throws β goes to error handler
res.json(users);
});This patches Express internally. No wrapper needed.
Solution 4: Express 5 (Automatic)
Express 5 (currently RC) handles async errors natively:
npm install express@next// Express 5 β async errors automatically forwarded to next()
app.get('/users', async (req, res) => {
const users = await User.findAll();
res.json(users);
});Parallel Async Operations
app.get('/dashboard', asyncHandler(async (req, res) => {
// β Sequential β slow! (each awaits the previous)
const users = await fetchUsers();
const posts = await fetchPosts();
const stats = await fetchStats();
// β
Parallel β all run simultaneously
const [users, posts, stats] = await Promise.all([
fetchUsers(),
fetchPosts(),
fetchStats()
]);
res.json({ users, posts, stats });
}));Timeout Handling
function withTimeout(promise, ms = 5000) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error(`Request timed out after ${ms}ms`)), ms)
);
return Promise.race([promise, timeout]);
}
app.get('/slow', asyncHandler(async (req, res) => {
const data = await withTimeout(slowExternalAPI(), 3000);
res.json(data);
}));Best Practices
- Always use
asyncHandlerwrapper ORexpress-async-errors - Never swallow errors β always call
next(err)or let the wrapper do it - Use
Promise.all()for independent async operations (faster) - Set timeouts for external API calls
- Use Express 5 for new projects β native async support
Summary
Express doesn't automatically handle async errors. Use an asyncHandler wrapper, express-async-errors, or migrate to Express 5 to safely use async/await in your route handlers. Always pass errors to next(err) so your central error handler can process them.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.