export class AppError extends Error {
public readonly statusCode: number;
public readonly code: string;
public readonly isOperational: boolean;
constructor(message: string, statusCode = 500, code = 'INTERNAL_ERROR', isOperational = true) {
super(message);
this.name = this.constructor.name;
this.statusCode = statusCode;
this.code = code;
this.isOperational = isOperational;
Error.captureStackTrace(this, this.constructor);
}
}
export class NotFoundError extends AppError {
constructor(message = 'Resource not found') {
super(message, 404, 'NOT_FOUND');
}
}
export class BadRequestError extends AppError {
constructor(message = 'Bad request', code = 'BAD_REQUEST') {
super(message, 400, code);
}
}
export interface FieldError {
field: string;
message: string;
}
export class ValidationError extends AppError {
public readonly details: FieldError[];
constructor(details: FieldError[], message = 'Validation failed') {
super(message, 422, 'VALIDATION_ERROR');
this.details = details;
}
}
import { Request, Response, NextFunction, RequestHandler } from 'express';
type AsyncRoute = (req: Request, res: Response, next: NextFunction) => Promise<unknown>;
export function asyncHandler(fn: AsyncRoute): RequestHandler {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
import { Request, Response, NextFunction } from 'express';
import { AppError, ValidationError } from './AppError';
import { logger } from './logger';
interface ErrorBody {
error: {
code: string;
message: string;
details?: unknown;
};
}
export function errorHandler(err: unknown, req: Request, res: Response, _next: NextFunction) {
const isProd = process.env.NODE_ENV === 'production';
const appError =
err instanceof AppError
? err
: new AppError(err instanceof Error ? err.message : 'Unexpected error', 500, 'INTERNAL_ERROR', false);
if (!appError.isOperational) {
logger.error('Unhandled error', { path: req.path, method: req.method, stack: appError.stack });
} else {
logger.warn('Operational error', { path: req.path, code: appError.code, message: appError.message });
}
const body: ErrorBody = {
error: {
code: appError.code,
message: !appError.isOperational && isProd ? 'Something went wrong' : appError.message,
},
};
if (appError instanceof ValidationError) {
body.error.details = appError.details;
}
res.status(appError.statusCode).json(body);
}
import express, { Request, Response, NextFunction } from 'express';
import { asyncHandler } from './asyncHandler';
import { errorHandler } from './errorHandler';
import { NotFoundError, ValidationError } from './AppError';
import { userRepo } from './userRepo';
export const app = express();
app.use(express.json());
app.get(
'/users/:id',
asyncHandler(async (req: Request, res: Response) => {
const user = await userRepo.findById(req.params.id);
if (!user) throw new NotFoundError(`user ${req.params.id} not found`);
res.json(user);
})
);
app.post(
'/users',
asyncHandler(async (req: Request, res: Response) => {
const { email } = req.body ?? {};
if (typeof email !== 'string' || !email.includes('@')) {
throw new ValidationError([{ field: 'email', message: 'must be a valid email' }]);
}
const user = await userRepo.create({ email });
res.status(201).json(user);
})
);
app.use((req: Request, _res: Response, next: NextFunction) => {
next(new NotFoundError(`route ${req.method} ${req.path} not found`));
});
app.use(errorHandler);
This snippet shows how a Node/Express API can funnel every failure — thrown exceptions, rejected promises, validation errors, unknown 500s — through a single terminal error middleware so responses are consistent and logging happens in exactly one place. The central idea is to separate operational errors (a missing resource, a bad request the client caused) from programmer errors (a null dereference, an unexpected bug), and to render both as a stable JSON envelope.
In AppError the base class extends the native Error and carries an HTTP statusCode, a machine-readable code, and an isOperational flag. The subclasses NotFoundError, BadRequestError, and ValidationError just fix those fields, which keeps call sites terse: throwing new NotFoundError('user not found') is enough to produce a 404 with the right body. ValidationError additionally attaches a details array so field-level messages survive the trip to the handler. Setting this.name and calling Error.captureStackTrace preserves clean stack traces despite subclassing.
The asyncHandler wrapper solves a classic Express footgun: async route handlers that reject are not caught by Express, so an await that throws silently hangs the request. Wrapping a handler routes any rejection into next(err), which is what actually reaches the error middleware. Every async route should be wrapped rather than sprinkling try/catch everywhere.
errorHandler middleware is the single terminal handler, identified by its four-argument signature (err, req, res, next) — Express only treats it as an error handler with all four params present. It normalizes anything non-AppError into a generic 500 marked non-operational, logs unexpected errors with full stack while logging operational ones at a quieter level, and hides internal messages in production so implementation details never leak to clients. The code field gives frontends something to branch on that is more durable than parsing prose.
app wiring ties it together: routes throw freely, notFoundHandler converts unmatched paths into a NotFoundError, and errorHandler is registered last so it catches everything downstream. The trade-off is discipline — handlers must throw typed errors rather than calling res.status().json() ad hoc — but in return every error path is uniform, testable, and observable from one location.
Related snips
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
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
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
createdAt: String!
GraphQL API with Spring Boot
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
Share this code
Here's the card — post it anywhere.