Error Handler
Centralized API errors with safe logging
Error Handler
Add a production-safe error layer for your API.
You get:
- consistent JSON error responses
- custom error classes
- async handler wrapper
- 404 handler
- safe error logging (sensitive fields redacted)
Installation
npx zuro-cli add error-handlerpnpm dlx zuro-cli add error-handlerbun x zuro-cli add error-handlerWhat Gets Added
errors.ts
error-handler.ts
app.ts
The CLI also wires this into app.ts:
app.use(notFoundHandler);
app.use(errorHandler);Error Response Contract
All errors use the same base shape:
{
status: "error";
code: string;
message: string;
requestId?: string;
errors?: { path: string; message: string }[];
stack?: string;
}Standard Error
{
"status": "error",
"code": "NOT_FOUND",
"message": "User not found"
}Validation Error
{
"status": "error",
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"errors": [
{ "path": "body.email", "message": "Invalid email" }
]
}Development Mode
In NODE_ENV=development, stack is included.
Request ID Support
If request id is available (for example from x-request-id header), it is included in:
- error response (
requestId) - error logs (
requestId)
Example response:
{
"status": "error",
"code": "INTERNAL_ERROR",
"message": "Something went wrong",
"requestId": "req_12345"
}Safe Logging Behavior
Error logs redact sensitive fields from request body, including keys like:
passwordtokenauthorizationcookiesecret
So logs stay useful without leaking secrets.
Usage
Throw Typed Errors
import { NotFoundError, ForbiddenError } from "./lib/errors";
if (!user) {
throw new NotFoundError("User not found");
}
if (!canEdit) {
throw new ForbiddenError("You cannot edit this resource");
}Wrap Route Handlers
import { Router } from "express";
import { asyncHandler } from "./middleware/error-handler";
import { UserController } from "./controllers/user.controller";
const router = Router();
router.get("/:id", asyncHandler(UserController.getUser));
router.post("/", asyncHandler(UserController.createUser));asyncHandler supports both sync and async handlers.
Available Error Classes
| Class | Status | Default Code |
|---|---|---|
BadRequestError | 400 | BAD_REQUEST |
UnauthorizedError | 401 | UNAUTHORIZED |
ForbiddenError | 403 | FORBIDDEN |
NotFoundError | 404 | NOT_FOUND |
ConflictError | 409 | CONFLICT |
ValidationError | 422 | VALIDATION_ERROR |
InternalServerError | 500 | INTERNAL_ERROR |
You can override the error code:
throw new NotFoundError("User not found", "USER_NOT_FOUND");Custom Error Example
import { AppError } from "./lib/errors";
export class RateLimitError extends AppError {
constructor(message = "Too many requests") {
super(message, 429, "RATE_LIMIT");
}
}Integration Notes
validatorreturns validation responses directly by default.- If a
ZodErrorreaches the error handler, it is formatted asVALIDATION_ERRORwith status422. - If headers were already sent, middleware safely delegates to Express (
next(err)).