npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

rfc9457

v16.0.0

Published

RFC 9457 Problem Details for HTTP APIs - A standardized error handling package for Node.js

Readme

RFC9457

TypeScript-first error handling package implementing RFC 9457 Problem Details for HTTP APIs.

RFC 9457 obsoletes RFC 7807 - this package implements the latest specification.

Features

  • 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+

Installation

npm install rfc9457

Quick Start

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");

Available Errors

Client Errors (4xx)

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") |

Server Errors (5xx)

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") |

Usage Examples

Basic Error Throwing

import { errors } from "rfc9457";

if (!user) {
  throw errors.client.notFound("User", userId);
}

if (!hasPermission) {
  throw errors.client.authorization("Admin access required");
}

Auto-Normalization

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");
}

Validation Errors

import { errors } from "rfc9457";

const validationErrors = {
  email: ["Invalid email format", "Email already exists"],
  password: ["Password too weak"],
};

throw errors.client.validation("Validation failed", validationErrors);

NotFound with Auto-formatting

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");

Using Convenient Aliases

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);
});

Additional Utilities

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
}

Framework Integration

Express

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());
});

Hono

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);
});

Fastify

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());
});

Configuration

Set the base URL for error type URIs using the environment variable:

export RFC9457_BASE_URL=https://api.example.com/errors

Error Response Format

All 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"]
  }
}

TypeScript Support

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: "...", ... }
}

API Reference

Categorized Errors (Recommended)

import { errors } from "rfc9457";

throw errors.client.badRequest("Invalid input");
throw errors.server.internal("System error");

Flat API (Alternative)

import { error } from "rfc9457";

throw error.badRequest("Invalid input");
throw error.internal("System error");

Helpers

import { isHttpError } from "rfc9457";

if (isHttpError(err)) {
  console.log(err.status);
  console.log(err.toJSON());
}

Development

npm install
npm run build
npm run lint

License

MIT