TypeScript-first error handling package implementing RFC 9457 Problem Details for HTTP APIs.
RFC 9457 obsoletes RFC 7807 - this package implements the latest specification.
- RFC 9457 Compliant - Strictly follows the Problem Details specification
- TypeScript First - Full type safety with excellent IDE support
- Auto-normalization - Accepts
unknownerrors and normalizes them automatically - Categorized API - Clean, readable error handling with
errors.client.*anderrors.server.* - 39 Standard HTTP Errors - Complete coverage of all standard HTTP error codes
- Convenient Aliases - Common shortcuts like
errors.server.db()for frequent use cases - Built-in Middleware - Ready-to-use error handlers for popular frameworks (Hono, and more coming)
- Framework Agnostic - Works with Express, Hono, Fastify, and any Node.js framework
- Zero Dependencies - Lightweight with no external dependencies (middleware have optional peer dependencies)
- ESM Only - Modern ES Modules for Node.js 22+
npm install rfc9457import { errors } from "rfc9457";
// Client errors
throw errors.client.authentication("Invalid token");
throw errors.client.notFound("User", "123");
throw errors.client.validation("Email is required");
// Server errors
throw errors.server.internal("Database connection failed");
throw errors.server.db("Connection pool exhausted");28 client error types covering all standard 4xx HTTP status codes:
| Method | Status | Description | Example |
|---|---|---|---|
badRequest |
400 | Malformed request | errors.client.badRequest("Invalid JSON") |
authentication |
401 | Missing/invalid credentials | errors.client.authentication("Token expired") |
paymentRequired |
402 | Payment required | errors.client.paymentRequired("Subscription required") |
authorization |
403 | Insufficient permissions | errors.client.authorization("Admin access required") |
notFound |
404 | Resource not found | errors.client.notFound("User", "123") |
methodNotAllowed |
405 | HTTP method not allowed | errors.client.methodNotAllowed("POST not allowed") |
notAcceptable |
406 | Not acceptable | errors.client.notAcceptable("Only JSON supported") |
proxyAuthenticationRequired |
407 | Proxy auth required | errors.client.proxyAuthenticationRequired("Proxy auth needed") |
requestTimeout |
408 | Request timeout | errors.client.requestTimeout("Request took too long") |
conflict |
409 | Resource conflict | errors.client.conflict("Email already exists") |
gone |
410 | Resource permanently deleted | errors.client.gone("Resource permanently deleted") |
lengthRequired |
411 | Length header required | errors.client.lengthRequired("Content-Length required") |
preconditionFailed |
412 | Precondition failed | errors.client.preconditionFailed("ETag mismatch") |
payloadTooLarge |
413 | Payload too large | errors.client.payloadTooLarge("File too large", 5000000) |
uriTooLong |
414 | URI too long | errors.client.uriTooLong("URL exceeds maximum length") |
unsupportedMediaType |
415 | Unsupported media type | errors.client.unsupportedMediaType("Only image/* allowed") |
rangeNotSatisfiable |
416 | Range not satisfiable | errors.client.rangeNotSatisfiable("Invalid byte range") |
expectationFailed |
417 | Expectation failed | errors.client.expectationFailed("Expect header failed") |
misdirectedRequest |
421 | Misdirected request | errors.client.misdirectedRequest("Wrong server") |
validation |
422 | Invalid input data | errors.client.validation("Invalid email", { email: ["Invalid format"] }) |
locked |
423 | Resource locked | errors.client.locked("Resource is locked") |
failedDependency |
424 | Failed dependency | errors.client.failedDependency("Dependency failed") |
tooEarly |
425 | Too early | errors.client.tooEarly("Request too early") |
upgradeRequired |
426 | Upgrade required | errors.client.upgradeRequired("Upgrade to TLS required") |
preconditionRequired |
428 | Precondition required | errors.client.preconditionRequired("If-Match header required") |
rateLimit |
429 | Too many requests | errors.client.rateLimit("Rate limit exceeded", 60) |
requestHeaderFieldsTooLarge |
431 | Headers too large | errors.client.requestHeaderFieldsTooLarge("Request headers too large") |
unavailableForLegalReasons |
451 | Legal restriction | errors.client.unavailableForLegalReasons("Blocked by legal order") |
11 server error types plus convenient aliases:
| Method | Status | Description | Example |
|---|---|---|---|
internal |
500 | Internal server error | errors.server.internal(caughtError) |
notImplemented |
501 | Feature not implemented | errors.server.notImplemented("Feature not available") |
badGateway |
502 | External service error | errors.server.badGateway(stripeError, "Stripe") |
serviceUnavailable |
503 | Service temporarily unavailable | errors.server.serviceUnavailable("Maintenance mode", 60) |
gatewayTimeout |
504 | External service timeout | errors.server.gatewayTimeout("Payment timeout", "Stripe") |
httpVersionNotSupported |
505 | HTTP version not supported | errors.server.httpVersionNotSupported("HTTP/2 required") |
variantAlsoNegotiates |
506 | Variant also negotiates | errors.server.variantAlsoNegotiates("Configuration error") |
insufficientStorage |
507 | Insufficient storage | errors.server.insufficientStorage("Out of storage space") |
loopDetected |
508 | Loop detected | errors.server.loopDetected("Circular dependency detected") |
notExtended |
510 | Not extended | errors.server.notExtended("Extension not supported") |
networkAuthenticationRequired |
511 | Network authentication required | errors.server.networkAuthenticationRequired("Proxy auth required") |
Convenient Aliases:
Common shortcuts for frequent use cases:
| Alias | Maps To | Status | Example |
|---|---|---|---|
| Client Aliases | |||
validate |
validation |
422 | errors.client.validate("Invalid email format") |
permission |
authorization |
403 | errors.client.permission("Access denied") |
access |
authorization |
403 | errors.client.access("Insufficient permissions") |
idNotFound |
notFound |
404 | errors.client.idNotFound("User", "123") |
duplicate |
conflict |
409 | errors.client.duplicate("Email already exists") |
thirdParty |
failedDependency |
424 | errors.client.thirdParty("External service failed") |
| Server Aliases | |||
db |
serviceUnavailable |
503 | errors.server.db("Connection pool exhausted") |
fetch |
badGateway |
502 | errors.server.fetch("GitHub API unreachable") |
envNotSet |
notImplemented |
501 | errors.server.envNotSet("DATABASE_URL not configured") |
envInvalid |
notImplemented |
501 | errors.server.envInvalid("DATABASE_URL must be a valid URL") |
maintenance |
serviceUnavailable |
503 | errors.server.maintenance("System under maintenance") |
migration |
insufficientStorage |
507 | errors.server.migration("Migration storage limit exceeded") |
unhandledRejection |
internal |
500 | errors.server.unhandledRejection("Unhandled promise rejection") |
uncaughtException |
internal |
500 | errors.server.uncaughtException("Uncaught exception") |
import { errors } from "rfc9457";
if (!user) {
throw errors.client.notFound("User", userId);
}
if (!hasPermission) {
throw errors.client.authorization("Admin access required");
}The package automatically normalizes any value to a string:
import { errors } from "rfc9457";
try {
await externalAPI.call();
} catch (err) {
throw errors.server.badGateway(err, "External API");
}import { errors } from "rfc9457";
const validationErrors = {
email: ["Invalid email format", "Email already exists"],
password: ["Password too weak"],
};
throw errors.client.validation("Validation failed", validationErrors);import { errors } from "rfc9457";
// Auto-formatted message: "User 123 not found"
throw errors.client.notFound("User", "123");
// Custom message
throw errors.client.notFound("Custom message: User not found in database");import { errors } from "rfc9457";
// Validation errors
if (!email.includes("@")) {
throw errors.client.validate("Invalid email format");
}
// Permission checks
if (!user.isAdmin) {
throw errors.client.permission("Admin access required");
}
// ID lookups
const user = await db.findUser(userId);
if (!user) {
throw errors.client.idNotFound("User", userId);
}
// Duplicate entries
if (await db.emailExists(email)) {
throw errors.client.duplicate("User with this email already exists");
}
// Database errors
try {
await db.query("SELECT * FROM users");
} catch (err) {
throw errors.server.db(err);
}
// External API failures
try {
await fetch("https://api.github.com/users/octocat");
} catch (err) {
throw errors.server.fetch(err);
}
// Third-party integrations
try {
await stripe.customers.create({ email });
} catch (err) {
throw errors.client.thirdParty(err);
}
// Environment configuration - missing
if (!process.env.DATABASE_URL) {
throw errors.server.envNotSet("DATABASE_URL environment variable not set");
}
// Environment configuration - invalid value
if (process.env.NODE_ENV && !["development", "production", "test"].includes(process.env.NODE_ENV)) {
throw errors.server.envInvalid("NODE_ENV must be development, production, or test");
}
// Maintenance mode
if (isMaintenanceMode) {
throw errors.server.maintenance("System is under scheduled maintenance", 3600);
}
// Migration failures
try {
await runMigration();
} catch (err) {
throw errors.server.migration(err);
}
// Node.js process error handlers
process.on('unhandledRejection', (reason) => {
console.error(errors.server.unhandledRejection(reason));
process.exit(1);
});
process.on('uncaughtException', (error) => {
console.error(errors.server.uncaughtException(error));
process.exit(1);
});import { errors, isValidRFC9457Json } from "rfc9457";
// Create error by status code
throw errors.byStatus(404, "Not found");
// Get JSON without throwing
const json = errors.client.badRequest("Invalid input").toJSON();
// Validate RFC 9457 response
if (isValidRFC9457Json(data)) {
// Valid RFC 9457 error
}import express from "express";
import { errors, isHttpError } from "rfc9457";
const app = express();
app.get("/users/:id", async (req, res) => {
const user = await db.users.findById(req.params.id);
if (!user) {
throw errors.client.notFound("User", req.params.id);
}
res.json(user);
});
app.use((err, req, res, next) => {
if (isHttpError(err)) {
return res.status(err.status).json(err.toJSON());
}
const internalError = errors.server.internal(err);
res.status(500).json(internalError.toJSON());
});Option 1: Using the built-in middleware (recommended)
import { Hono } from "hono";
import { errors, honoErrorMiddleware } from "rfc9457";
const app = new Hono();
app.get("/users/:id", async (c) => {
const user = await db.users.findById(c.req.param("id"));
if (!user) {
throw errors.client.notFound("User", c.req.param("id"));
}
return c.json(user);
});
app.onError(honoErrorMiddleware);Option 2: Manual error handling
import { Hono } from "hono";
import { errors, isHttpError } from "rfc9457";
const app = new Hono();
app.get("/users/:id", async (c) => {
const user = await db.users.findById(c.req.param("id"));
if (!user) {
throw errors.client.notFound("User", c.req.param("id"));
}
return c.json(user);
});
app.onError((err, c) => {
if (isHttpError(err)) {
return c.json(err.toJSON(), err.status);
}
const internalError = errors.server.internal(err);
return c.json(internalError.toJSON(), 500);
});Process Error Handlers
import { errors } from "rfc9457";
process.on('unhandledRejection', (reason) => {
console.error(errors.server.unhandledRejection(reason));
process.exit(1);
});
process.on('uncaughtException', (error) => {
console.error(errors.server.uncaughtException(error));
process.exit(1);
});import Fastify from "fastify";
import { errors, isHttpError } from "rfc9457";
const fastify = Fastify();
fastify.get("/users/:id", async (request, reply) => {
const user = await db.users.findById(request.params.id);
if (!user) {
throw errors.client.notFound("User", request.params.id);
}
return user;
});
fastify.setErrorHandler((error, request, reply) => {
if (isHttpError(error)) {
return reply.status(error.status).send(error.toJSON());
}
const internalError = errors.server.internal(error);
reply.status(500).send(internalError.toJSON());
});Set the base URL for error type URIs using the environment variable:
export RFC9457_BASE_URL=https://api.example.com/errorsAll errors follow RFC 9457 structure:
{
"type": "about:blank#not-found",
"title": "Not Found",
"status": 404,
"detail": "User 123 not found"
}With custom base URL:
{
"type": "https://api.example.com/errors/validation",
"title": "Validation Error",
"status": 422,
"detail": "Validation failed",
"validationErrors": {
"email": ["Invalid email format"]
}
}Full type safety and IDE autocomplete:
import { errors, isHttpError } from "rfc9457";
const err = errors.client.validation("Invalid data");
if (isHttpError(err)) {
console.log(err.status); // 422
console.log(err.toJSON()); // { type: "...", title: "...", ... }
}import { errors } from "rfc9457";
throw errors.client.badRequest("Invalid input");
throw errors.server.internal("System error");import { error } from "rfc9457";
throw error.badRequest("Invalid input");
throw error.internal("System error");import { isHttpError } from "rfc9457";
if (isHttpError(err)) {
console.log(err.status);
console.log(err.toJSON());
}npm install
npm run build
npm run lintMIT