Error Handler
Centralized, type-safe error handling pipeline for Express applications with a typed AppError class and error catalog.
The Error Handler block gives your Express application a single, predictable place to handle every error — expected or not.
It separates errors you anticipate (bad input, missing resources, failed auth) from bugs you don't, returning a clean JSON response for the former and a safe, logged fallback for the latter.
Features
- Centralized Express error-handling middleware
- Typed
AppErrorclass with status code and operational flag - Shared error catalog for consistent messages across your app
- Built-in Zod validation error formatting
- Async route wrapper so rejected promises never get swallowed
- Zero database assumptions
- TypeScript support
File Structure
error-handler
├── app-error.ts
├── errors.ts
├── http-status.ts
├── throw-error.ts
├── async-handler.ts
├── global-error-handler.ts
└── index.ts- app-error.ts — Base
AppErrorclass that all expected application errors extend. - errors.ts — Shared error catalog (
ERRORS) with consistent message and HTTP status mappings. - http-status.ts — Named HTTP status code constants.
- throw-error.ts — Utility to throw an
AppErrorfrom a catalog entry. - async-handler.ts — Wraps async route handlers so rejected promises reach the error handler.
- global-error-handler.ts — Express 4-argument error-handling middleware.
- index.ts — Public exports.
Installation
pnpm dlx blockend-cli add error-handlerDetect Configuration
Blockend reads your project's language, framework, and alias setup.
Install Dependencies
zod is required for validation error formatting. Blockend installs it automatically if it isn't already present.
Generate Files
The block is generated inside your configured blocks directory.
Copy the files below into your project's blocks directory.
Peer Dependencies
| Dependency | Required for |
|---|---|
express | Express middleware |
zod | Validation error formatting |
@types/express | TypeScript types (dev) |
blocks/error-handler/app-error.ts
Base class for all expected, handled application errors.
/**
* Base class for all expected, handled application errors.
*
* Use this for errors you anticipate — bad input, missing resources,
* auth failures. The global error handler checks `instanceof AppError`
* and returns a clean, predictable JSON shape for these.
*
* Anything that is NOT an AppError is treated as a bug, logged in full,
* and never leaked to the client beyond a generic message.
*/
export class AppError extends Error {
constructor(
public readonly statusCode: number,
message: string,
/**
* Operational errors are expected and safe to expose to the client.
* Set to false for errors that are technically anticipated but
* shouldn't reveal details (rare — defaults to true for normal use).
*/
public readonly isOperational: boolean = true
) {
super(message);
this.name = "AppError";
// Required when extending built-ins like Error in TypeScript —
// without this, `instanceof AppError` can silently return false
// depending on the consuming project's compile target.
Object.setPrototypeOf(this, AppError.prototype);
}
}
blocks/error-handler/http-status.ts
Named HTTP status code constants.
export const HTTP_STATUS = {
OK: 200,
CREATED: 201,
BAD_REQUEST: 400,
UNAUTHORIZED: 401,
FORBIDDEN: 403,
NOT_FOUND: 404,
CONFLICT: 409,
UNPROCESSABLE_ENTITY: 422,
INTERNAL_SERVER_ERROR: 500,
SERVICE_UNAVAILABLE: 503
} as const;
export type HttpStatus = (typeof HTTP_STATUS)[keyof typeof HTTP_STATUS];
blocks/error-handler/errors.ts
Common error catalog. Use with throwError(ERRORS.NOT_FOUND) or reference .message / .status directly.
import { HTTP_STATUS } from "./http-status.js";
/**
* Common error catalog. Use with `throwError(ERRORS.NOT_FOUND)` or
* reference `.message` / `.status` directly when building your own AppError.
*
* This is a starting set, not an exhaustive one — add your own entries
* as your domain needs them. It's your file now.
*/
export const ERRORS = {
VALIDATION_FAILED: {
message: "Validation failed",
status: HTTP_STATUS.BAD_REQUEST
},
BAD_REQUEST: {
message: "Bad request",
status: HTTP_STATUS.BAD_REQUEST
},
INVALID_CREDENTIALS: {
message: "Invalid credentials",
status: HTTP_STATUS.UNAUTHORIZED
},
UNAUTHORIZED: {
message: "Unauthorized access",
status: HTTP_STATUS.UNAUTHORIZED
},
FORBIDDEN: {
message: "You do not have permission to perform this action",
status: HTTP_STATUS.FORBIDDEN
},
TOKEN_EXPIRED: {
message: "Session expired. Please log in again.",
status: HTTP_STATUS.UNAUTHORIZED
},
INVALID_TOKEN: {
message: "Invalid token",
status: HTTP_STATUS.UNAUTHORIZED
},
TOKEN_TYPE_MISMATCH: {
message: "Token type mismatch",
status: HTTP_STATUS.UNAUTHORIZED
},
TOKEN_REVOKED: {
message: "Token has been revoked",
status: HTTP_STATUS.UNAUTHORIZED
},
NOT_FOUND: {
message: "Not found",
status: HTTP_STATUS.NOT_FOUND
},
USER_ALREADY_EXISTS: {
message: "User already exists",
status: HTTP_STATUS.CONFLICT
},
DUPLICATE_RESOURCE: {
message: "Resource already exists",
status: HTTP_STATUS.CONFLICT
},
UNPROCESSABLE: {
message: "Unable to process the request",
status: HTTP_STATUS.UNPROCESSABLE_ENTITY
},
INTERNAL_SERVER_ERROR: {
message: "Internal server error",
status: HTTP_STATUS.INTERNAL_SERVER_ERROR
},
SERVICE_UNAVAILABLE: {
message: "Service temporarily unavailable",
status: HTTP_STATUS.SERVICE_UNAVAILABLE
}
} as const;
export type ErrorKey = keyof typeof ERRORS;
blocks/error-handler/throw-error.ts
Throws an AppError from a catalog entry.
import { AppError } from "./app-error.js";
import { ERRORS } from "./errors.js";
/**
* Throws an AppError from a catalog entry in ERRORS.
*
* Usage: throwError(ERRORS.NOT_FOUND)
* Equivalent to: throw new AppError(404, 'Not found')
*
* Use this for catalog errors. Use `new AppError(status, message)`
* directly for one-off errors that don't belong in the shared catalog.
*/
export function throwError(error: { message: string; status: number }): never {
throw new AppError(error.status, error.message);
}
export { ERRORS };
blocks/error-handler/async-handler.ts
Wraps an async Express route handler so rejected promises are passed to next().
import type { Request, Response, NextFunction } from "express";
/**
* Wraps an async Express route handler so rejected promises are passed
* to `next()` and reach the global error handler, instead of crashing
* the process or hanging the request.
*
* Pairs directly with globalErrorHandler — use both together.
*
* router.get('/users/:id', asyncHandler(async (req, res) => {
* const user = await db.user.findUnique({ where: { id: req.params.id } })
* if (!user) throwError(ERRORS.NOT_FOUND)
* res.json(user)
* }))
*/
export const asyncHandler =
(fn: (req: Request, res: Response, next: NextFunction) => Promise<unknown>) =>
(req: Request, res: Response, next: NextFunction): void => {
Promise.resolve(fn(req, res, next)).catch(next);
};
blocks/error-handler/global-error-handler.ts
Express error-handling middleware. Register this last, after all routes.
import type { Request, Response, NextFunction } from "express";
import { z, ZodError } from "zod";
import { AppError } from "./app-error.js";
import { HTTP_STATUS } from "./http-status.js";
import { ERRORS } from "./errors.js";
/**
* Express error-handling middleware. Register this LAST, after all routes
* and other middleware — Express identifies error handlers by their
* 4-argument signature, and only calls the first one that matches.
*
* app.use(globalErrorHandler)
*/
export const globalErrorHandler = (
err: unknown,
req: Request,
res: Response,
// oxlint-disable-next-line @typescript-eslint/no-unused-vars
next: NextFunction
) => {
// -------------------------
// Custom AppError — expected, operational errors
// -------------------------
if (err instanceof AppError) {
return res.status(err.statusCode).json({
success: false,
data: null,
message: err.message
});
}
// -------------------------
// Zod validation error
// -------------------------
if (err instanceof ZodError) {
return res.status(HTTP_STATUS.BAD_REQUEST).json({
success: false,
data: null,
message: ERRORS.VALIDATION_FAILED.message,
errors: z.treeifyError(err)
});
}
// -------------------------
// Unknown / unhandled errors — never leak details to the client
// -------------------------
logUnhandledError(err, req);
return res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({
success: false,
data: null,
message: ERRORS.INTERNAL_SERVER_ERROR.message
});
};
/**
* Single seam for unhandled-error logging. Swap this out for a
* structured logger (e.g. Blockend's `logger` block) without touching
* the control flow above.
*/
function logUnhandledError(err: unknown, req: Request): void {
// oxlint-disable-next-line no-console
console.error("[UNHANDLED ERROR]", {
method: req.method,
path: req.path,
error: err
});
}
blocks/error-handler/index.ts
Public exports — re-exports all public APIs.
export { AppError } from "./app-error.js";
export { ERRORS } from "./errors.js";
export type { ErrorKey } from "./errors.js";
export { HTTP_STATUS } from "./http-status.js";
export type { HttpStatus } from "./http-status.js";
export { throwError } from "./throw-error.js";
export { globalErrorHandler } from "./global-error-handler.js";
export { asyncHandler } from "./async-handler.js";
Configuration
| Export | Type | Description |
|---|---|---|
AppError | class | Base error class — new AppError(statusCode, message, isOperational?) |
ERRORS | Record<string, { message: string; status: number }> | Shared catalog of common errors |
HTTP_STATUS | Record<string, number> | Named HTTP status code constants |
throwError | (error: { message, status }) => never | Throws an AppError from a catalog entry |
globalErrorHandler | Express ErrorRequestHandler | The 4-argument middleware — register last |
asyncHandler | (fn) => RequestHandler | Wraps async route handlers to forward rejections to next() |
Architecture
Request
│
▼
Route Handler (optionally wrapped with asyncHandler)
│
▼
Error thrown (AppError / ZodError / unknown)
│
▼
globalErrorHandler
├── AppError → 4xx JSON response
├── ZodError → 400 JSON with field errors
└── Unknown → 500 JSON + server-side logThe globalErrorHandler is registered last in your Express middleware stack. It checks the error type and returns the appropriate response. AppError instances return clean JSON with the configured status code. ZodError instances are formatted using z.treeifyError. All other errors are logged server-side and returned as a generic 500 response.
When to Use
- You want a centralized place to handle every error in your Express application.
- You need consistent JSON error responses across your API.
- You use Zod for validation and want automatic error formatting.
When Not to Use
- You don't use Express.
- You prefer handling errors ad-hoc in each route handler.
- You don't need structured error responses.
Usage
Setup
Register the handler last, after all your routes and middleware:
import express from "express";
import { globalErrorHandler, asyncHandler, throwError, ERRORS } from "@/blocks/error-handler";
const app = express();
app.get(
"/users/:id",
asyncHandler(async (req, res) => {
if (!req.params.id) throwError(ERRORS.BAD_REQUEST);
const user = await db.user.findUnique({ where: { id: req.params.id } });
if (!user) throwError(ERRORS.NOT_FOUND);
res.json(user);
})
);
app.use(globalErrorHandler); // Must be last
app.listen(3000);Throwing Errors
Use the catalog for common cases, AppError for one-off errors:
import { throwError, ERRORS, AppError } from "@/blocks/error-handler";
// From the catalog — consistent messages across your API
throwError(ERRORS.NOT_FOUND);
// One-off error with a custom message
throw new AppError(400, `Unsupported plan: ${plan}`);Validation Errors
Zod errors are automatically formatted when they reach globalErrorHandler:
import { z } from "zod";
import { asyncHandler } from "@/blocks/error-handler";
const createUserSchema = z.object({
email: z.string().email(),
password: z.string().min(8)
});
router.post(
"/users",
asyncHandler(async (req, res) => {
const data = createUserSchema.parse(req.body); // throws ZodError on failure
const user = await db.user.create({ data });
res.status(201).json(user);
})
);Response on failure:
{
"success": false,
"data": null,
"message": "Validation failed",
"errors": {
"properties": {
"email": { "errors": ["Invalid email"] }
}
}
}Mapping Database Errors
Map database-specific errors to AppError in your data layer:
router.post(
"/users",
asyncHandler(async (req, res, next) => {
try {
const user = await db.user.create({ data: req.body });
res.status(201).json(user);
} catch (err) {
if (isUniqueConstraintError(err)) {
return next(new AppError(409, "A user with this email already exists"));
}
next(err);
}
})
);API Reference
AppError
class AppError extends Error {
constructor(
public readonly statusCode: number,
message: string,
public readonly isOperational: boolean = true
);
}| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
statusCode | number | Yes | — | HTTP status code |
message | string | Yes | — | Error message |
isOperational | boolean | No | true | Whether the error is safe to expose to the client |
ERRORS
| Key | Status | Message |
|---|---|---|
VALIDATION_FAILED | 400 | Validation failed |
BAD_REQUEST | 400 | Bad request |
INVALID_CREDENTIALS | 401 | Invalid credentials |
UNAUTHORIZED | 401 | Unauthorized access |
FORBIDDEN | 403 | You do not have permission to perform this action |
TOKEN_EXPIRED | 401 | Session expired. Please log in again. |
INVALID_TOKEN | 401 | Invalid token |
TOKEN_TYPE_MISMATCH | 401 | Token type mismatch |
TOKEN_REVOKED | 401 | Token has been revoked |
NOT_FOUND | 404 | Not found |
USER_ALREADY_EXISTS | 409 | User already exists |
DUPLICATE_RESOURCE | 409 | Resource already exists |
UNPROCESSABLE | 422 | Unable to process the request |
INTERNAL_SERVER_ERROR | 500 | Internal server error |
SERVICE_UNAVAILABLE | 503 | Service temporarily unavailable |
throwError
function throwError(error: { message: string; status: number }): never;Throws an AppError from a catalog entry. Use for catalog errors; use new AppError(status, message) directly for one-off errors.
globalErrorHandler
const globalErrorHandler: (err: unknown, req: Request, res: Response, next: NextFunction) => void;Express 4-argument error-handling middleware. Register last.
asyncHandler
const asyncHandler: (
fn: (req: Request, res: Response, next: NextFunction) => Promise<unknown>
) => (req: Request, res: Response, next: NextFunction) => void;Wraps async route handlers so rejected promises are forwarded to next().
HTTP_STATUS
const HTTP_STATUS: {
OK: 200;
CREATED: 201;
BAD_REQUEST: 400;
UNAUTHORIZED: 401;
FORBIDDEN: 403;
NOT_FOUND: 404;
CONFLICT: 409;
UNPROCESSABLE_ENTITY: 422;
INTERNAL_SERVER_ERROR: 500;
SERVICE_UNAVAILABLE: 503;
};Examples
Extending the Error Catalog
Add domain-specific errors to the shared catalog:
// blocks/error-handler/errors.ts
export const ERRORS = {
...existingEntries,
SUBSCRIPTION_EXPIRED: {
message: "Your subscription has expired",
status: HTTP_STATUS.FORBIDDEN
}
} as const;Related Blocks
- Logger — Replace the internal
console.errorcall with a structured logger for production error tracking. - Response Formatter — Format error responses consistently across your API.
FAQ
What happens when an unhandled error reaches globalErrorHandler?
It logs the error server-side with console.error including the method, path, and error details. The client receives a generic 500 response — no stack traces or internal details are leaked.
How do I add my own error types?
Create entries in the ERRORS catalog or throw new AppError(status, message) directly for one-off errors.
Does this block work with databases?
No. It makes no database assumptions. Map database-specific errors to AppError in your own data layer.