RFC 9457 Problem Details for HTTP APIs - A standardized error handling package for Node.js
npm install rfc9457TypeScript-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 unknown errors and normalizes them automatically
- Categorized API - Clean, readable error handling with errors.client. and errors.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+
``bash`
npm install rfc9457
`typescript
import { 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") |db
| Server Aliases | | | |
| | 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") |
`typescript
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:
`typescript
import { errors } from "rfc9457";
try {
await externalAPI.call();
} catch (err) {
throw errors.server.badGateway(err, "External API");
}
`
`typescript
import { errors } from "rfc9457";
const validationErrors = {
email: ["Invalid email format", "Email already exists"],
password: ["Password too weak"],
};
throw errors.client.validation("Validation failed", validationErrors);
`
`typescript
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");
`
`typescript
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);
});
`
`typescript
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
}
`
`typescript
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)
`typescript
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
`typescript
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
`typescript
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);
});
`
`typescript
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:
`bash`
export RFC9457_BASE_URL=https://api.example.com/errors
All errors follow RFC 9457 structure:
`json`
{
"type": "about:blank#not-found",
"title": "Not Found",
"status": 404,
"detail": "User 123 not found"
}
With custom base URL:
`json`
{
"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:
`typescript
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: "...", ... }
}
`
`typescript
import { errors } from "rfc9457";
throw errors.client.badRequest("Invalid input");
throw errors.server.internal("System error");
`
`typescript
import { error } from "rfc9457";
throw error.badRequest("Invalid input");
throw error.internal("System error");
`
`typescript
import { isHttpError } from "rfc9457";
if (isHttpError(err)) {
console.log(err.status);
console.log(err.toJSON());
}
`
`bash``
npm install
npm run build
npm run lint
MIT