ZZuro Docs

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-handler
pnpm dlx zuro-cli add error-handler
bun x zuro-cli add error-handler

What 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:

  • password
  • token
  • authorization
  • cookie
  • secret

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

ClassStatusDefault Code
BadRequestError400BAD_REQUEST
UnauthorizedError401UNAUTHORIZED
ForbiddenError403FORBIDDEN
NotFoundError404NOT_FOUND
ConflictError409CONFLICT
ValidationError422VALIDATION_ERROR
InternalServerError500INTERNAL_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

  • validator returns validation responses directly by default.
  • If a ZodError reaches the error handler, it is formatted as VALIDATION_ERROR with status 422.
  • If headers were already sent, middleware safely delegates to Express (next(err)).

Next Steps

On this page