Handling Async Operations
1. Using async/await in Route Handlers
Example: Express 5 native
app.get("/users/:id", async (req, res) => {
const user = await db.users.findById(req.params.id);
if (!user) throw new HttpError(404);
res.json(user);
});
| Express Version | Auto-forward errors? |
| 4.x | No — wrap or use express-async-errors |
| 5.x | Yes (since 5.0) |
2. Creating Async Middleware
Example: Async auth
export async function authenticate(req, res, next) {
const token = (req.get("Authorization") || "").replace("Bearer ", "");
if (!token) throw new HttpError(401);
const payload = await verifyJwt(token);
req.user = await db.users.findById(payload.sub);
next();
}
3. Handling Promise Rejections
| Source | Express 5 behavior |
| Awaited rejection | Forwarded to error MW |
| Unawaited promise | NOT caught — fires on unhandledRejection |
| Setimer / event callback | NOT caught — wrap manually |
Warning: Always await promises inside handlers, or attach .catch(next). Fire-and-forget promises bypass Express error handling.
4. Using express-async-handler Package
Example: Wrapper utility
import asyncHandler from "express-async-handler";
app.get("/users", asyncHandler(async (req, res) => {
const users = await db.users.findAll();
res.json(users);
}));
5. Implementing try-catch Wrappers
Example: Inline try/catch
app.get("/data", async (req, res, next) => {
try {
const data = await fetchExternal();
res.json(data);
} catch (err) {
next(err); // delegate to error middleware
}
});
6. Forwarding Async Errors
| Pattern | Notes |
| Throw in async handler | Express 5: auto-forwarded |
| next(err) | Explicit forward |
| Promise.catch(next) | Tail-call forward |
7. Using Promise.all() for Parallel Operations
Example: Parallel fetch
app.get("/dashboard", async (req, res) => {
const [user, stats, notifications] = await Promise.all([
db.users.findById(req.user.id),
statsService.fetch(req.user.id),
notificationService.list(req.user.id)
]);
res.json({ user, stats, notifications });
});
| Method | Behavior |
Promise.all([...]) | Reject on first failure |
Promise.allSettled([...]) | Wait for all (no rejection) |
Promise.race([...]) | Resolve with first settled |
Promise.any([...]) | First fulfilled (skip rejected) |
8. Implementing Async Validation
Example: zod async refinement
const userSchema = z.object({
email: z.string().email().refine(
async (email) => !(await db.users.findByEmail(email)),
{ message: "Email already in use" }
)
});
const data = await userSchema.parseAsync(req.body);
9. Handling Async Database Operations
Example: Transaction
app.post("/transfer", async (req, res) => {
const { from, to, amount } = req.body;
await db.$transaction(async (tx) => {
await tx.account.update({ where: { id: from }, data: { balance: { decrement: amount } } });
await tx.account.update({ where: { id: to }, data: { balance: { increment: amount } } });
});
res.sendStatus(204);
});
10. Using Promise.allSettled() for Multiple Operations
Example: Best-effort parallel
app.post("/notify", async (req, res) => {
const results = await Promise.allSettled([
sendEmail(req.body),
sendSms(req.body),
sendPush(req.body)
]);
const failures = results
.map((r, i) => ({ channel: ["email","sms","push"][i], ok: r.status === "fulfilled", reason: r.reason?.message }))
.filter(r => !r.ok);
res.json({ sent: results.length - failures.length, failures });
});