javascript 118 lines · 4 tabs

Centralized Async Error Handling in Express With asyncHandler and Error Middleware

Shared by codesnips Aug 2026
4 tabs
class ApiError extends Error {
  constructor(statusCode, message, code) {
    super(message);
    this.name = 'ApiError';
    this.statusCode = statusCode;
    this.code = code || null;
    this.isApiError = true;
    Error.captureStackTrace(this, this.constructor);
  }

  static badRequest(message, code) {
    return new ApiError(400, message || 'Bad Request', code);
  }

  static notFound(message, code) {
    return new ApiError(404, message || 'Not Found', code);
  }

  static conflict(message, code) {
    return new ApiError(409, message || 'Conflict', code);
  }
}

function asyncHandler(fn) {
  return function wrapped(req, res, next) {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
}

module.exports = { asyncHandler, ApiError };
4 files · javascript Explain with highlit

This snippet shows the standard Express pattern for taming async route errors: a small asyncHandler wrapper plus a single centralized error-handling middleware, so route code never needs a try/catch at all. The problem it solves is specific to Express — when an async route handler rejects, the returned promise is not awaited by the router, so the rejection is unhandled and the request hangs until it times out. Express only routes to error middleware when next(err) is called or when a synchronous throw is caught, and neither happens for a rejected promise.

In asyncHandler.js, the wrapper takes a route function and returns a new handler that invokes it, then calls Promise.resolve(...).catch(next). Any thrown error or rejected promise is funneled into next, which Express recognizes as an error and forwards to error middleware. This keeps every handler flat and readable while guaranteeing errors are never swallowed.

The ApiError class gives the codebase a typed error carrying an HTTP statusCode and an optional machine-readable code. Static helpers like ApiError.notFound and ApiError.badRequest make throwing an intentional, well-formed error a one-liner, which is far cleaner than sprinkling res.status(404).json(...) throughout controllers.

usersController.js demonstrates the payoff: each handler is wrapped once with asyncHandler, awaits its data access, and simply throws an ApiError for the not-found case. There is no error plumbing in the business logic — a missing user throws, and the wrapper routes it onward.

Finally, errorHandler.js is the four-argument middleware (err, req, res, next) that Express treats specially. It distinguishes known ApiError instances from unexpected failures, defaulting the latter to 500 and hiding internal messages in production to avoid leaking stack traces. It also handles the already-sent-headers edge case by delegating to next(err), since writing to a committed response would crash the process. A notFoundHandler converts unmatched routes into a consistent ApiError too. The ordering matters: both middlewares must be registered after all routes so they act as the final safety net. The trade-off is a light convention every handler must follow, in exchange for uniform error shapes and zero repetitive try/catch.


Related snips

typescript
export interface RetryOptions {
  retries: number;
  baseMs: number;
  maxMs: number;
  signal?: AbortSignal;
  onRetry?: (attempt: number, delay: number, err: unknown) => void;

Exponential backoff with jitter for retries

typescript reliability retry
by codesnips 2 tabs
go
package dbutil

import (
  "context"

  "github.com/jackc/pgconn"

Retry Postgres serialization failures with bounded attempts

go postgres transactions
by Leah Thompson 1 tab
ruby
class SignupForm
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :account_name, :string
  attribute :email, :string

Shallow Controller, Deep Params: Form Object Pattern

rails activemodel form-object
by codesnips 3 tabs
javascript
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
  const success = true;

  setTimeout(() => {
    if (success) {

Promises and async/await patterns for asynchronous JavaScript

javascript promises async-await
by Alex Chang 1 tab
typescript
export type Settled<R> =
  | { status: 'fulfilled'; value: R }
  | { status: 'rejected'; reason: unknown };

export interface ConcurrencyOptions {
  limit: number;

Simple concurrency limiter for batch operations

node concurrency async
by codesnips 2 tabs
typescript
import { randomBytes, createHash } from "crypto";
import jwt from "jsonwebtoken";
import { RefreshTokenStore } from "./store";

const ACCESS_SECRET = process.env.ACCESS_SECRET!;
const ACCESS_TTL = "15m";

JWT access + refresh token rotation (conceptual)

security node jwt
by codesnips 3 tabs

Share this code

Here's the card — post it anywhere.

Centralized Async Error Handling in Express With asyncHandler and Error Middleware — share card
Link copied