http-fault
v1.0.2
Published
Lightweight, extensible HTTP exceptions with proper status codes and framework-agnostic error handling for Node.js.
Maintainers
Readme
http-fault
A lightweight, framework-agnostic TypeScript library for creating and handling structured HTTP exceptions.
Features
- Full 4xx/5xx coverage — one class per HTTP status code
- Structured responses — consistent JSON shape across every error
- Framework agnostic — works with Express, Fastify, Koa, Hono, raw
node:http, or anything built on it - Safe by default — 5xx details are hidden from clients unless explicitly exposed
- Extensible — create domain-specific exceptions by extending any class
- Zero dependencies — only uses
node:httpbuilt-in types
Installation
npm install http-faultFile Structure
http.exception.ts # Abstract base class
http.exceptions.ts # All 4xx and 5xx exception classes
exception-filter.ts # Framework-agnostic error handlerQuick Start
import { NotFoundException } from "http-fault";
throw new NotFoundException({ message: "User not found." });Every error is serialized to a consistent JSON response:
{
"status": 404,
"error": "Not Found",
"message": "User not found.",
"code": undefined,
"details": undefined,
"path": "/users/99",
"timestamp": "2026-02-28T12:00:00.000Z"
}HttpException Base Class
All exceptions extend HttpException, which extends Error.
abstract class HttpException extends Error {
readonly status: number; // HTTP status code
readonly error: string; // Standard HTTP error name
readonly message: string; // Human-readable description (inherited from Error)
readonly code?: string; // Optional machine-readable error code
readonly details?: unknown; // Optional structured payload
readonly expose: boolean; // Whether details are safe to send to the client
readonly cause?: Error; // Optional originating error
}Constructor Options
All exception constructors accept an optional options object:
| Option | Type | Description |
| --------- | --------- | -------------------------------------------------------------------------------------------- |
| message | string | Human-readable description. Defaults to the HTTP error name. |
| code | string | Machine-readable error code (e.g. "USER_NOT_FOUND"). |
| details | unknown | Structured payload (e.g. validation errors). |
| expose | boolean | Whether message and details reach the client. Auto-set: true for 4xx, false for 5xx. |
| cause | Error | The originating error, for error chaining. |
Built-in Exceptions
4xx Client Errors
| Class | Status |
| -------------------------------------- | ------ |
| BadRequestException | 400 |
| UnauthorizedException | 401 |
| PaymentRequiredException | 402 |
| ForbiddenException | 403 |
| NotFoundException | 404 |
| MethodNotAllowedException | 405 |
| NotAcceptableException | 406 |
| ProxyAuthenticationRequiredException | 407 |
| RequestTimeoutException | 408 |
| ConflictException | 409 |
| GoneException | 410 |
| LengthRequiredException | 411 |
| PreconditionFailedException | 412 |
| ContentTooLargeException | 413 |
| URITooLongException | 414 |
| UnsupportedMediaTypeException | 415 |
| RangeNotSatisfiableException | 416 |
| ExpectationFailedException | 417 |
| ImATeapotException | 418 |
| MisdirectedRequestException | 421 |
| UnprocessableEntityException | 422 |
| LockedException | 423 |
| FailedDependencyException | 424 |
| TooEarlyException | 425 |
| UpgradeRequiredException | 426 |
| PreconditionRequiredException | 428 |
| TooManyRequestsException | 429 |
| RequestHeaderFieldsTooLargeException | 431 |
| UnavailableForLegalReasonsException | 451 |
5xx Server Errors
| Class | Status |
| ---------------------------------------- | ------ |
| InternalServerErrorException | 500 |
| NotImplementedException | 501 |
| BadGatewayException | 502 |
| ServiceUnavailableException | 503 |
| GatewayTimeoutException | 504 |
| HTTPVersionNotSupportedException | 505 |
| VariantAlsoNegotiatesException | 506 |
| InsufficientStorageException | 507 |
| LoopDetectedException | 508 |
| NotExtendedException | 510 |
| NetworkAuthenticationRequiredException | 511 |
Throwing Exceptions
import {
NotFoundException,
ForbiddenException,
UnprocessableEntityException,
ConflictException,
} from "http-fault";
// Minimal — just a status
throw new NotFoundException();
// With a message
throw new NotFoundException({ message: "User 42 not found." });
// With a machine-readable code
throw new ForbiddenException({
message: "Only admins can perform this action.",
code: "INSUFFICIENT_ROLE",
});
// With structured details (great for validation errors)
throw new UnprocessableEntityException({
message: "Validation failed.",
code: "VALIDATION_FAILED",
details: {
email: ["Must be a valid email address."],
age: ["Must be at least 18."],
},
});
// With error chaining
try {
await db.insert(user);
} catch (cause) {
throw new ConflictException({
message: "A user with this email already exists.",
code: "EMAIL_TAKEN",
cause,
});
}Custom Exceptions
Extend any built-in class to create reusable domain exceptions.
import {
UnauthorizedException,
TooManyRequestsException,
HttpException,
} from "http-fault";
// Fix the code and default message for a specific auth scenario
class TokenExpiredException extends UnauthorizedException {
constructor(cause?: Error) {
super({
message: "Your session has expired. Please sign in again.",
code: "TOKEN_EXPIRED",
cause,
});
}
}
// Carry typed runtime data in details
class QuotaExceededException extends TooManyRequestsException {
constructor(used: number, limit: number) {
super({
message: `Daily quota exceeded (${used}/${limit}).`,
code: "QUOTA_EXCEEDED",
details: { used, limit },
});
}
}
// Build directly on HttpException for full control
class MaintenanceModeException extends HttpException {
constructor(until: string) {
super(503, "Service Unavailable", {
message: "The service is under scheduled maintenance.",
code: "MAINTENANCE_MODE",
details: { until },
expose: true, // safe to surface despite being 5xx
});
}
}
// Usage
throw new TokenExpiredException();
throw new QuotaExceededException(1000, 1000);
throw new MaintenanceModeException("2026-03-01T06:00:00Z");ExceptionFilter
The ExceptionFilter converts any thrown value into a structured JSON response. It handles both HttpException instances and unexpected raw errors.
API
// One-off call
ExceptionFilter(err, req, res, options?)
// Pre-configured factory — bind options once, reuse everywhere
const handleError = createExceptionFilter(options?)
handleError(err, req, res)Options
| Option | Type | Description |
| ------------------ | ------------------------- | ----------------------------------------------------------------------------- |
| onError | (err, response) => void | Custom logger. Called for all errors if provided, or only 5xx by default. |
| resolvePath | (req) => string | Override how the request path is extracted. Defaults to req.url ?? "/". |
| forceHideDetails | boolean | Always hide message and details, regardless of expose. Useful in tests. |
Integrations
Raw Node.js
import http from "node:http";
import { ExceptionFilter } from "http-fault";
http
.createServer(async (req, res) => {
try {
await router(req, res);
} catch (err) {
ExceptionFilter(err, req, res);
}
})
.listen(3000);Express
import express from "express";
import { ExceptionFilter } from "http-fault";
const app = express();
app.get("/users/:id", (req, res) => {
throw new NotFoundException({ message: `User ${req.params.id} not found.` });
});
// Register last
app.use((err, req, res, _next) => ExceptionFilter(err, req, res));Fastify
import Fastify from "fastify";
import { ExceptionFilter } from "http-fault";
const fastify = Fastify();
fastify.setErrorHandler((err, req, reply) => {
ExceptionFilter(err, req.raw, reply.raw);
});Koa
import Koa from "koa";
import { ExceptionFilter } from "http-fault";
const app = new Koa();
app.on("error", (err, ctx) => {
ExceptionFilter(err, ctx.req, ctx.res);
});Custom Logger
import pino from "pino";
import { createExceptionFilter } from "http-fault";
const logger = pino();
export const handleError = createExceptionFilter({
onError: (err, response) => {
if (response.status >= 500) {
logger.error({ err, response }, "Unhandled server error");
} else {
logger.warn({ response }, "Client error");
}
},
});
// app.use((err, req, res, _next) => handleError(err, req, res));The expose Flag
This flag controls whether message and details are sent to the client.
| Scenario | expose default | What the client sees |
| ---------------- | ---------------- | --------------------------------- |
| 4xx exception | true | Full message and details |
| 5xx exception | false | Only the standard HTTP error name |
| Custom exception | Your choice | Depends on expose option |
// Client receives the full message and details (expose: true by default for 4xx)
throw new UnprocessableEntityException({
message: "Email is invalid.",
details: { field: "email" },
});
// → { message: "Email is invalid.", details: { field: "email" }, ... }
// Client receives only "Internal Server Error" (expose: false by default for 5xx)
throw new InternalServerErrorException({
message: "Database connection lost at 192.168.1.5:5432",
});
// → { message: "Internal Server Error", details: undefined, ... }
// Force-expose a 5xx (use with care)
throw new ServiceUnavailableException({
message: "Back online at 03:00 UTC.",
expose: true,
});
// → { message: "Back online at 03:00 UTC.", ... }Error Response Shape
Every error response follows this structure:
{
status: number // HTTP status code
error: string // Standard HTTP error name
message: string // Human-readable description (gated by expose)
code?: string // Machine-readable code (gated by expose)
details?: unknown // Structured payload (gated by expose)
path: string // Request path
timestamp: string // ISO 8601 timestamp
}Testing
serializeException is a pure function — no server required.
import { serializeException, NotFoundException } from "http-fault";
it("returns 404 with message and code", () => {
const err = new NotFoundException({
message: "Post not found.",
code: "POST_NOT_FOUND",
});
const result = serializeException(err, "/posts/99");
expect(result).toMatchObject({
status: 404,
error: "Not Found",
message: "Post not found.",
code: "POST_NOT_FOUND",
path: "/posts/99",
});
});
it("hides details when forceHideDetails is true", () => {
const err = new NotFoundException({ message: "Hidden." });
const result = serializeException(err, "/", true);
expect(result.message).toBe("Not Found"); // message hidden
expect(result.details).toBeUndefined();
});
it("maps unknown errors to 500", () => {
const result = serializeException(new Error("boom"), "/");
expect(result.status).toBe(500);
});License
MIT
