HTTP-friendly error objects for Hono, inspired by Boom.
[!WARNING]
This package is not production-ready. It is under active development with daily updates and could change significantly.
- Installation
- Overview
- Key Features
- Usage
- Formatters
- API Reference
- Best Practices
- Contributing
- License
npm install hono-ban
hono-ban provides a comprehensive error handling solution for Hono.js applications. It offers a set of factory functions for creating HTTP-friendly error objects, middleware for handling errors, and utilities for error conversion and formatting.
- Type-Safe Error Creation: Factory functions for all standard HTTP error codes (4xx and 5xx)
- Middleware Integration: Seamless integration with Hono's middleware system
- Flexible Error Data: Support for custom error data and metadata
- Error Conversion: Convert any error to a standardized format
- Customizable Formatting: Extensible error formatting capabilities
- Standard Formatters: Built-in support for common error formats like RFC7807 Problem Details
- Security Features: Built-in sanitization to prevent sensitive data leakage
- Developer-Friendly: Detailed stack traces in development, sanitized in production
import { Hono } from "hono";
import ban, { notFound, badRequest } from "hono-ban";
// Create a Hono app
const app = new Hono();
// IMPORTANT: Add the ban middleware for error handling to work
app.use(ban());
// Create a 404 Not Found error
const error = notFound("User not found");
// Create a 400 Bad Request error with custom data
const validationError = badRequest("Invalid input", {
data: {
invalidFields: ["email", "password"],
},
});
// Example route that throws an error
app.get("/users/:id", (c) => {
// The thrown error will be caught and formatted by the ban middleware
throw notFound(`User with ID ${c.req.param("id")} not found`);
});
Note: The ban middleware is required for errors to be automatically caught and formatted. Without it, thrown errors won't be properly handled.
The ban middleware is required to catch and format errors thrown in your routes. Without this middleware, errors will not be properly handled.
import { Hono } from "hono";
import ban from "hono-ban";
import { notFound } from "hono-ban";
const app = new Hono();
// Add the error handling middleware with default options
// This is REQUIRED for error handling to work
app.use(ban());
// Routes can throw errors that will be handled automatically
app.get("/users/:id", (c) => {
const user = findUser(c.req.param("id"));
if (!user) {
throw notFound(`User with ID ${c.req.param("id")} not found`);
}
return c.json(user);
});
import { Hono } from "hono";
import ban from "hono-ban";
import { notFound } from "hono-ban";
const app = new Hono();
// Add the error handling middleware with advanced configuration
app.use(
ban({
formatter: customFormatter, // Custom error formatter
sanitize: ["password", "token"], // Fields to remove from error output
includeStackTrace: process.env.NODE_ENV !== "production", // Only include stack traces in development
headers: { "X-Powered-By": "hono-ban" }, // Default headers to include in error responses
})
);
// Routes can throw errors that will be handled automatically
app.get("/users/:id", (c) => {
const user = findUser(c.req.param("id"));
if (!user) {
throw notFound(`User with ID ${c.req.param("id")} not found`);
}
return c.json(user);
});
import { badRequest } from "hono-ban";
// Add custom data to your errors
const error = badRequest("Validation failed", {
data: {
field: "email",
reason: "Invalid format",
suggestion: "Use a valid email address",
},
headers: {
"X-Error-Code": "VAL_001",
},
});
import { convertToBanError, isBanError } from "hono-ban";
try {
// Some operation that might throw
await someOperation();
} catch (err) {
// Convert any error to a BanError
const banError = convertToBanError(err, {
statusCode: 500,
message: "An error occurred during processing",
});
// Check if an error is a BanError
if (isBanError(err)) {
console.log("Status code:", err.status);
}
}
import { formatError, createErrorResponse, defaultFormatter } from "hono-ban";
import type { ErrorFormatter } from "hono-ban";
// Create a custom formatter
const myFormatter: ErrorFormatter = {
contentType: "application/json",
format(error, headers, sanitize, includeStackTrace) {
return {
code: error.status,
message: error.message,
timestamp: new Date().toISOString(),
details: error.data,
};
},
};
// Format an error using the custom formatter
const formatted = formatError(banError, myFormatter, {
includeStackTrace: true,
});
// Create a Response from the formatted error
const response = createErrorResponse(banError, formatted);
hono-ban integrates seamlessly with @hono/zod-openapi to provide standardized error handling for OpenAPI-validated routes.
import { OpenAPIHono } from "@hono/zod-openapi";
import { ban, createRFC7807Formatter, rfc7807Hook } from "hono-ban";
import { RFC7807DetailsSchema } from "hono-ban/formatters/rfc7807";
import { createRoute, z } from "@hono/zod-openapi";
import type { Context } from "hono";
import type { Env } from "./config/env";
// Create an OpenAPIHono instance with the RFC7807 hook for validation errors
const app = new OpenAPIHono<Env>({ defaultHook: rfc7807Hook });
// IMPORTANT: Add the ban middleware with RFC7807 formatter
app.use(
ban({
formatter: createRFC7807Formatter({
baseUrl: "https://api.example.com/errors",
}),
})
);
// Define a schema for your data
const NoteSchema = z.object({
name: z.string().max(10),
});
// Create an OpenAPI route with error handling
const route = createRoute({
method: "post",
path: "/notes",
request: {
body: {
content: {
"application/json": {
schema: NoteSchema,
},
},
required: true,
},
},
responses: {
200: {
description: "Successfully created note",
content: {
"application/json": {
schema: z.object({
data: z.object({
id: z.string(),
type: z.string(),
attributes: NoteSchema,
}),
}),
},
},
},
400: {
description: "Validation error",
content: {
"application/json": {
schema: RFC7807DetailsSchema, // Use the RFC7807 schema for errors
},
},
},
},
});
// Register the route
app.openapi(route, async (c) => {
// Handle the request
// Any validation errors will be automatically formatted using RFC7807
return c.json({
data: {
/* response data */
},
});
});
-
Automatic Validation Error Handling: The
rfc7807Hook
automatically converts Zod validation errors to RFC7807 format. - Standardized Error Responses: All errors follow the RFC7807 specification.
- OpenAPI Documentation: Error schemas are properly documented in your OpenAPI specification.
- Type Safety: Full TypeScript support for request and response validation.
Note: The ban middleware is still required when using OpenAPI integration. The
rfc7807Hook
handles validation errors, but the middleware is needed to catch and format other errors.
hono-ban includes several built-in formatters for common error response formats.
The default formatter produces a clean, flat JSON structure with status code, error name, and optional message and data.
import { defaultFormatter } from "hono-ban";
// Example output:
// {
// "statusCode": 400,
// "error": "Bad Request",
// "message": "Invalid input",
// "data": { "field": "email" }
// }
The RFC7807 formatter implements the RFC 7807: Problem Details for HTTP APIs specification.
import { createRFC7807Formatter, createValidationError } from "hono-ban";
// or more specifically:
import {
createRFC7807Formatter,
createValidationError,
} from "hono-ban/formatters/rfc7807";
// Create the formatter
const formatter = createRFC7807Formatter({
baseUrl: "https://api.example.com/problems",
});
// Use with middleware
app.use(ban({ formatter }));
// Create validation error data
app.post("/users", (c) => {
const { email } = await c.req.json();
if (!isValidEmail(email)) {
throw badRequest("Invalid input", {
data: createValidationError([
{ name: "email", reason: "Must be a valid email" },
]),
});
}
// Process valid request...
});
{
"type": "https://api.example.com/problems/400",
"title": "Bad Request",
"status": 400,
"detail": "Invalid input",
"instance": "urn:uuid:...",
"timestamp": "2024-02-20T12:00:00.000Z",
"invalid-params": [
{
"name": "email",
"reason": "Must be a valid email"
}
]
}
The RFC7807 formatter provides several helper functions:
-
createValidationError(params)
: Create validation error data -
createZodValidationError(error)
: Convert Zod validation errors to RFC7807 format -
createConstraintViolation(name, reason, resource, constraint)
: Create constraint violation data -
createRFC7807Hook(options)
: Create a Hono hook for Zod OpenAPI validation (see OpenAPI Integration for usage)
hono-ban provides factory functions for all standard HTTP error codes:
-
badRequest(messageOrOptions?, options?)
: 400 Bad Request -
unauthorized(messageOrOptions?, options?)
: 401 Unauthorized -
paymentRequired(messageOrOptions?, options?)
: 402 Payment Required -
forbidden(messageOrOptions?, options?)
: 403 Forbidden -
notFound(messageOrOptions?, options?)
: 404 Not Found -
methodNotAllowed(messageOrOptions?, options?)
: 405 Method Not Allowed -
notAcceptable(messageOrOptions?, options?)
: 406 Not Acceptable -
proxyAuthRequired(messageOrOptions?, options?)
: 407 Proxy Authentication Required -
clientTimeout(messageOrOptions?, options?)
: 408 Request Timeout -
conflict(messageOrOptions?, options?)
: 409 Conflict -
resourceGone(messageOrOptions?, options?)
: 410 Gone -
lengthRequired(messageOrOptions?, options?)
: 411 Length Required -
preconditionFailed(messageOrOptions?, options?)
: 412 Precondition Failed -
entityTooLarge(messageOrOptions?, options?)
: 413 Payload Too Large -
uriTooLong(messageOrOptions?, options?)
: 414 URI Too Long -
unsupportedMediaType(messageOrOptions?, options?)
: 415 Unsupported Media Type -
rangeNotSatisfiable(messageOrOptions?, options?)
: 416 Range Not Satisfiable -
expectationFailed(messageOrOptions?, options?)
: 417 Expectation Failed -
teapot(messageOrOptions?, options?)
: 418 I'm a Teapot -
misdirectedRequest(messageOrOptions?, options?)
: 421 Misdirected Request -
badData(messageOrOptions?, options?)
: 422 Unprocessable Entity -
locked(messageOrOptions?, options?)
: 423 Locked -
failedDependency(messageOrOptions?, options?)
: 424 Failed Dependency -
tooEarly(messageOrOptions?, options?)
: 425 Too Early -
upgradeRequired(messageOrOptions?, options?)
: 426 Upgrade Required -
preconditionRequired(messageOrOptions?, options?)
: 428 Precondition Required -
tooManyRequests(messageOrOptions?, options?)
: 429 Too Many Requests -
headerFieldsTooLarge(messageOrOptions?, options?)
: 431 Request Header Fields Too Large -
illegal(messageOrOptions?, options?)
: 451 Unavailable For Legal Reasons
-
internal(messageOrOptions?, options?)
: 500 Internal Server Error -
notImplemented(messageOrOptions?, options?)
: 501 Not Implemented -
badGateway(messageOrOptions?, options?)
: 502 Bad Gateway -
serverUnavailable(messageOrOptions?, options?)
: 503 Service Unavailable -
gatewayTimeout(messageOrOptions?, options?)
: 504 Gateway Timeout -
httpVersionNotSupported(messageOrOptions?, options?)
: 505 HTTP Version Not Supported -
variantAlsoNegotiates(messageOrOptions?, options?)
: 506 Variant Also Negotiates -
insufficientStorage(messageOrOptions?, options?)
: 507 Insufficient Storage -
loopDetected(messageOrOptions?, options?)
: 508 Loop Detected -
notExtended(messageOrOptions?, options?)
: 510 Not Extended -
networkAuthRequired(messageOrOptions?, options?)
: 511 Network Authentication Required -
badImplementation(messageOrOptions?, options?)
: 500 Internal Server Error (marked as developer error)
-
createError(options)
: Create a new error object with the given options -
convertToBanError(err, options?)
: Convert any error into a BanError -
isBanError(err, statusCode?)
: Type guard to check if a value is a BanError -
formatError(error, formatter, options?)
: Format an error using the provided formatter -
createErrorResponse(error, formatted)
: Create a Response object from a formatted error
-
ban(options?)
: Create error handling middleware with the specified options
interface BanError<T = unknown> {
status: ErrorStatusCode;
message: string;
data?: T;
headers?: Record<string, string>;
allow?: readonly string[];
stack?: string;
cause?: unknown;
causeStack?: string;
readonly isBan: true;
}
interface BanOptions<T = unknown> {
statusCode?: ErrorStatusCode;
message?: string;
data?: T;
headers?: Record<string, string>;
allow?: string | string[];
cause?: Error | unknown;
formatter?: ErrorFormatter;
sanitize?: readonly string[];
includeStackTrace?: boolean;
}
interface BanMiddlewareOptions {
formatter?: ErrorFormatter;
sanitize?: readonly string[];
includeStackTrace?: boolean;
headers?: Record<string, string>;
}
interface ErrorFormatter<T = unknown> {
readonly contentType: string;
format(
error: BanError,
headers?: Record<string, string>,
sanitize?: readonly string[],
includeStackTrace?: boolean
): T;
}
- Use Specific Error Types: Use the most specific error factory function that matches your use case.
// Good
throw notFound("User not found");
// Less specific
throw createError({ statusCode: 404, message: "User not found" });
- Include Meaningful Data: Add context to your errors to help with debugging and user feedback.
throw badRequest("Invalid input", {
data: {
field: "email",
reason: "Invalid format",
expected: "valid@example.com",
},
});
- Security Considerations: Sanitize sensitive data in production environments.
app.use(
ban({
sanitize: ["password", "token", "secret"],
includeStackTrace: process.env.NODE_ENV !== "production",
})
);
-
Developer Errors: Use
badImplementation
for errors that should never happen in production.
if (!database) {
throw badImplementation("Database connection not initialized");
}
-
Reuse Formatters: Create formatters once and reuse them rather than creating new ones for each request.
-
Selective Stack Traces: Only include stack traces in development to reduce response size in production.
We welcome contributions! Please see the main project's Contributing Guide for details.
MIT License - see the LICENSE file for details.