JSPM

http-fault

1.0.2
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 17
  • Score
    100M100P100Q37214F
  • License MIT

Lightweight, extensible HTTP exceptions with proper status codes and framework-agnostic error handling for Node.js.

Package Exports

  • http-fault

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:http built-in types

Installation

npm install http-fault

File Structure

http.exception.ts        # Abstract base class
http.exceptions.ts       # All 4xx and 5xx exception classes
exception-filter.ts      # Framework-agnostic error handler

Quick 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