Async Middleware Patterns — Complete Implementation Guide
In this tutorial, you will learn about Async Middleware Patterns. We cover key concepts, practical examples, and best practices to help you master this topic.
Async middleware patterns handle asynchronous operations in the middleware pipeline, ensuring promise rejections are caught and forwarded to error handlers instead of crashing the server.
What You'll Learn
By the end of this tutorial, you will write async middleware that handles database queries, external API calls, and file operations safely, with proper error propagation and cleanup.
Why It Matters
Express 4 does not catch promise rejections automatically. An unhandled rejection in async middleware crashes the entire Process. DodaTech wraps all async middleware with error handlers to prevent production outages.
Real-World Use
Doda Browser's sync API uses async middleware to fetch user data from databases, validate tokens against external auth services, and process file uploads, all with proper error handling.
Async Middleware Learning Path
flowchart LR
A[Middleware Chaining] --> B[Async Middleware]
B --> C[Error Wrappers]
C --> D[Express 5 Changes]
B --> E{You Are Here}
style E fill:#f90,color:#fff
The Async Problem
Async middleware that throws without catching causes an unhandled promise rejection, which Node.js treats as a fatal error.
const express = require("express");
const app = express();
// This will crash the server on error
app.get("/danger", async (req, res) => {
const data = await fetchData();
res.json(data);
});
function fetchData() {
return Promise.reject(new Error("Database connection failed"));
}
app.use((err, req, res, next) => {
console.error("This never runs:", err.message);
res.status(500).json({ error: "Server error" });
});
app.listen(3000);
Expected output: The server crashes with UnhandledPromiseRejectionWarning because the error handler does not catch the async rejection.
The Async Handler Wrapper
The standard solution wraps async route handlers to catch promise rejections and forward them to the error middleware.
const express = require("express");
const app = express();
function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
app.get("/safe", asyncHandler(async (req, res) => {
const data = await fetchData();
res.json(data);
}));
function fetchData() {
return Promise.reject(new Error("Database connection failed"));
}
app.use((err, req, res, next) => {
console.error("Caught by handler:", err.message);
res.status(500).json({ error: "Database error" });
});
app.listen(3000);
Expected output for GET /safe:
{"error": "Database error"}
Async Middleware with Cleanup
Some async middleware needs cleanup logic when errors occur, such as closing database connections or releasing file handles.
const express = require("express");
const app = express();
function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
function withTransaction(req, res, next) {
req.transaction = { id: Date.now(), active: true };
console.log(`Transaction ${req.transaction.id} started`);
const originalEnd = res.end.bind(res);
res.end = function (...args) {
console.log(`Transaction ${req.transaction.id} ended`);
req.transaction = null;
return originalEnd(...args);
};
next();
}
app.get("/data", withTransaction, asyncHandler(async (req, res) => {
const data = await queryDatabase(req.transaction);
res.json(data);
}));
function queryDatabase() {
return Promise.resolve(["item1", "item2"]);
}
app.use((err, req, res, next) => {
if (req.transaction) {
console.log(`Cleaning up transaction ${req.transaction.id}`);
req.transaction = null;
}
res.status(500).json({ error: err.message });
});
app.listen(3000);
Express 5 Async Handling
Express 5 natively catches promise rejections in async middleware and route handlers, eliminating the need for wrapper functions.
// Express 5 only - works without wrapper
const express = require("express");
const app = express();
app.get("/native", async (req, res, next) => {
const data = await fetchData();
res.json(data);
});
app.use((err, req, res, next) => {
res.status(500).json({ error: err.message });
});
// Expected output: {"error": "Database error"}
app.listen(3000);
Common Mistakes
Not wrapping async middleware -- Without the wrapper, unhandled rejections crash the process. Always wrap async middleware in Express 4.
Throwing errors that are not Error instances -- Always throw
new Error("..."). Throwing strings or objects loses stack traces.Forgetting to use try/catch in async middleware -- Even with the wrapper, use try/catch for local error handling and cleanup before forwarding.
Calling next() after sending response -- This causes "headers already sent" errors. Return early after sending a response.
Not awaiting in middleware -- Forgetting
awaitcauses the promise to float. The middleware chain continues before the async operation completes.
Practice Questions
Why does Express 4 require a wrapper for async middleware? Express 4 does not catch promise rejections. The wrapper catches them and forwards to the error handler.
How does Express 5 change async error handling? Express 5 catches promise rejections natively and passes them to the error handler automatically.
What happens if async middleware does not call next() or send a response? The request hangs. The client waits until timeout. Always ensure async middleware calls next() or sends a response.
Challenge: Create an async middleware wrapper that also measures execution time.
function timedAsync(fn) {
return (req, res, next) => {
const start = Date.now();
Promise.resolve(fn(req, res, next)).catch(next).finally(() => {
console.log(`${req.url} took ${Date.now() - start}ms`);
});
};
}
FAQ
Mini Project
Build an async middleware system with automatic error wrapping, cleanup handlers, and performance monitoring.
const express = require("express");
const app = express();
function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
function withCleanup(fn) {
return asyncHandler(async (req, res, next) => {
try {
await fn(req, res, next);
} finally {
console.log(`Cleanup for ${req.url}`);
}
});
}
function timed(fn) {
return asyncHandler(async (req, res, next) => {
const start = Date.now();
await fn(req, res, next);
console.log(`${req.url} completed in ${Date.now() - start}ms`);
});
}
app.get("/users", timed(withCleanup(async (req, res) => {
const users = await db.query("SELECT * FROM users");
res.json(users);
})));
app.get("/error", timed(asyncHandler(async (req, res) => {
throw new Error("Test error");
})));
app.use((err, req, res, next) => {
console.error("Error:", err.message);
res.status(500).json({ error: err.message });
});
app.listen(3000);
What's Next
Now that you understand async middleware, explore integrating popular third-party middleware packages. Then learn about testing middleware functions in isolation.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro