Skip to content
Express.js ex typescript 4 min read

Typed Middleware & Handlers

Express handlers and middleware are just functions, but typing them well is what unlocks autocompletion on req, res, and next and stops subtle mistakes like forgetting to call next() or returning the wrong value. The community @types/express package ships dedicated function types — RequestHandler and ErrorRequestHandler — that describe these signatures precisely. This page shows how to use them, how to keep async handlers type-safe, and how to sidestep the next() pitfalls that trip up most TypeScript Express projects.

Typing a request handler

A standard middleware or route handler receives req, res, and next. Rather than annotating each parameter by hand, annotate the whole function with RequestHandler. This single type wires up all three parameters correctly and gives you a return type that matches what Express expects.

import { RequestHandler } from "express";

const requestLogger: RequestHandler = (req, res, next) => {
  console.log(`${req.method} ${req.path}`);
  next();
};

const getUser: RequestHandler = (req, res) => {
  res.json({ id: req.params.id, name: "Ada" });
};

RequestHandler is generic. Its parameters let you pin down route params, the response body, the request body, and the query string — in that order: RequestHandler<Params, ResBody, ReqBody, ReqQuery>. Supplying them turns req and res into fully typed objects.

import { RequestHandler } from "express";

interface UserParams { id: string }
interface UserBody { name: string; email: string }
interface UserResponse { id: string; name: string }

const updateUser: RequestHandler<UserParams, UserResponse, UserBody> = (
  req,
  res
) => {
  const { name } = req.body; // typed as string
  res.json({ id: req.params.id, name }); // body checked against UserResponse
};
Generic slotMaps toExample
Paramsreq.params{ id: string }
ResBodyres.json(...) argument{ id: string; name: string }
ReqBodyreq.body{ name: string }
ReqQueryreq.query{ page?: string }

Tip: Annotate the variable with RequestHandler rather than annotating req: Request, res: Response inline. The single annotation lets TypeScript infer the parameter types for you and keeps the generics in one readable place.

Typing error-handling middleware

Express recognises error handlers by their arity: a middleware function with four parameters (err, req, res, next) is treated as an error handler. The matching type is ErrorRequestHandler. Using it ensures you keep all four parameters — drop one and Express silently treats the function as ordinary middleware.

import { ErrorRequestHandler } from "express";

const errorHandler: ErrorRequestHandler = (err, req, res, next) => {
  const status = err instanceof RangeError ? 400 : 500;
  res.status(status).json({ error: err.message });
};

// Registered last, after all routes
app.use(errorHandler);

The err parameter is typed as any (or unknown under stricter configs) because anything can be thrown. Narrow it before use — check instanceof Error or a custom error class — instead of trusting that err.message exists.

Typing async wrappers

Async handlers are where typing and runtime behaviour most often diverge. In Express 4, a rejected promise inside an async handler is not caught automatically — the rejection escapes and crashes the process unless you forward it to next. The usual fix is a small wrapper, and typing it well preserves the handler’s own generics.

import { RequestHandler, Request, Response, NextFunction } from "express";

const asyncHandler =
  (fn: RequestHandler): RequestHandler =>
  (req: Request, res: Response, next: NextFunction) => {
    Promise.resolve(fn(req, res, next)).catch(next);
  };

Use it to wrap any async route, and rejected promises are routed straight to your error-handling middleware:

import express from "express";

const app = express();
app.use(express.json());

app.get(
  "/users/:id",
  asyncHandler(async (req, res) => {
    const user = await db.users.findById(req.params.id);
    if (!user) throw new Error("User not found");
    res.json(user);
  })
);

A request for a missing user now produces a clean error response through your handler rather than an unhandled rejection:

Output:

GET /users/999  ->  500 Internal Server Error
{ "error": "User not found" }

Express 5 changes this: it automatically forwards rejected promises from async handlers to next, so the wrapper becomes optional. It remains useful for consistent logging or when supporting both major versions.

Avoiding next() pitfalls

The most common typing mistake is returning the result of res or next. In a RequestHandler, the expected return type is void (or a promise of it), so writing return res.json(...) to end a function early can clash with strict lint rules and obscure control flow.

// Avoid: returning the response object
const bad: RequestHandler = (req, res, next) => {
  if (!req.headers.authorization) return res.status(401).end();
  next();
};

// Prefer: send, then return void
const good: RequestHandler = (req, res, next) => {
  if (!req.headers.authorization) {
    res.status(401).end();
    return;
  }
  next();
};

Two more rules keep middleware predictable: call next() exactly once per path through the function, and call next(err) (passing an argument) to jump to error handling rather than continuing the normal chain. Calling next() after already sending a response triggers an “ERR_HTTP_HEADERS_SENT” error at runtime — a separate return after every res.send/res.json prevents it.

Best Practices

  • Annotate handlers with RequestHandler and error handlers with ErrorRequestHandler instead of typing req/res parameters individually.
  • Use the four generic slots (Params, ResBody, ReqBody, ReqQuery) to make req.body, req.params, and res.json fully type-checked.
  • Wrap async handlers (or rely on Express 5’s built-in forwarding) so rejected promises reach your error middleware instead of crashing the process.
  • Keep next callbacks returning void — send the response, then return, rather than return res.json(...).
  • Call next() once per code path and next(err) to delegate to error handling; never call it after the response is already sent.
  • Narrow the err parameter in error handlers with instanceof checks before reading its properties.
Last updated June 14, 2026
Was this helpful?