Skip to content

Async Middleware Patterns — Complete Implementation Guide

DodaTech Updated 2026-06-28 5 min read

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

  1. Not wrapping async middleware -- Without the wrapper, unhandled rejections crash the process. Always wrap async middleware in Express 4.

  2. Throwing errors that are not Error instances -- Always throw new Error("..."). Throwing strings or objects loses stack traces.

  3. Forgetting to use try/catch in async middleware -- Even with the wrapper, use try/catch for local error handling and cleanup before forwarding.

  4. Calling next() after sending response -- This causes "headers already sent" errors. Return early after sending a response.

  5. Not awaiting in middleware -- Forgetting await causes the promise to float. The middleware chain continues before the async operation completes.

Practice Questions

  1. 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.

  2. How does Express 5 change async error handling? Express 5 catches promise rejections natively and passes them to the error handler automatically.

  3. 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.

  4. 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

Can I use try/catch in async middleware instead of a wrapper?

Yes. Wrap the body in try/catch and call next(err) in the catch block. The wrapper just eliminates repetitive try/catch blocks.

Does the async wrapper work for error-handling middleware?

No. Error-handling middleware has four parameters. The wrapper is for regular middleware and route handlers.

What happens to unhandled rejections in Node.js 15+?

Node.js 15+ treats unhandled rejections as fatal. The process exits with a non-zero code, which is why wrapping is critical.

Should I wrap every route handler individually?

Consider a global solution: override the Route prototype to wrap all handlers automatically, or use a library that monkey-patches Express.

How do I test async middleware error handling?

Pass a mock req that causes the middleware to throw, and assert that next() is called with the error.

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