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 };
const { asyncHandler, ApiError } = require('./asyncHandler');
const User = require('./models/User');
const listUsers = asyncHandler(async (req, res) => {
const page = Math.max(parseInt(req.query.page, 10) || 1, 1);
const users = await User.paginate({ page, perPage: 25 });
res.json(users);
});
const getUser = asyncHandler(async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) {
throw ApiError.notFound('User does not exist', 'user_not_found');
}
res.json(user);
});
const createUser = asyncHandler(async (req, res) => {
const { email, name } = req.body;
if (!email) {
throw ApiError.badRequest('email is required', 'email_missing');
}
const existing = await User.findByEmail(email);
if (existing) {
throw ApiError.conflict('email already registered', 'email_taken');
}
const user = await User.create({ email, name });
res.status(201).json(user);
});
module.exports = { listUsers, getUser, createUser };
const { ApiError } = require('./asyncHandler');
function notFoundHandler(req, res, next) {
next(ApiError.notFound(`Route ${req.method} ${req.originalUrl} not found`, 'route_not_found'));
}
function errorHandler(err, req, res, next) {
if (res.headersSent) {
return next(err);
}
const isKnown = err && err.isApiError === true;
const statusCode = isKnown ? err.statusCode : 500;
const inProduction = process.env.NODE_ENV === 'production';
if (!isKnown || statusCode >= 500) {
req.log ? req.log.error(err) : console.error(err);
}
const body = {
error: {
code: isKnown ? err.code : 'internal_error',
message: isKnown || !inProduction ? err.message : 'Internal Server Error'
}
};
if (!inProduction && !isKnown) {
body.error.stack = err.stack;
}
res.status(statusCode).json(body);
}
module.exports = { notFoundHandler, errorHandler };
const express = require('express');
const { listUsers, getUser, createUser } = require('./usersController');
const { notFoundHandler, errorHandler } = require('./errorHandler');
const app = express();
app.use(express.json());
app.get('/users', listUsers);
app.get('/users/:id', getUser);
app.post('/users', createUser);
// Registered after all routes so they catch everything.
app.use(notFoundHandler);
app.use(errorHandler);
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`listening on ${port}`);
});
module.exports = app;
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
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
package dbutil
import (
"context"
"github.com/jackc/pgconn"
Retry Postgres serialization failures with bounded attempts
class SignupForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :account_name, :string
attribute :email, :string
Shallow Controller, Deep Params: Form Object Pattern
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
const success = true;
setTimeout(() => {
if (success) {
Promises and async/await patterns for asynchronous JavaScript
export type Settled<R> =
| { status: 'fulfilled'; value: R }
| { status: 'rejected'; reason: unknown };
export interface ConcurrencyOptions {
limit: number;
Simple concurrency limiter for batch operations
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)
Share this code
Here's the card — post it anywhere.