Utilities for creating and working with Http Errors.
This package was inspired by http-errors for Node.js.
- Framework agnostic
- RFC 9457 Problem Details compliant error responses
- Zero runtime dependencies
- Written in TypeScript and published as ESM with type declarations
The package is published to npm
and JSR. It requires Node.js 22 or later,
and also works in browsers and other runtimes that provide the standard
Headers and Response classes.
# npm
npm install @udibo/http-error
# pnpm
pnpm add @udibo/http-error
# pnpm, from JSR (pnpm 10.9 or later)
pnpm add jsr:@udibo/http-error
# npm, from JSR
npx jsr add @udibo/http-errorThe package is ESM only. Import it from an ES module (or a TypeScript project
using "module": "NodeNext", "Bundler", or another ESM-aware setting):
import { HttpError } from "@udibo/http-error";Use it in any Node.js ES module. HttpError extends Error, so it works with
throw, try/catch, and error.cause like any other error.
import { HttpError } from "@udibo/http-error";
try {
throw new HttpError(404, "file not found");
} catch (error) {
const httpError = HttpError.from(error);
console.log(httpError.status); // 404
console.log(JSON.stringify(httpError)); // {"status":404,"title":"Not Found","detail":"file not found"}
}The package does not import any Node.js modules, so it can be bundled for
browsers (Vite, esbuild, webpack, and similar) or loaded directly as a module.
It is useful on the client for turning a failed fetch response into an error:
import { HttpError } from "@udibo/http-error";
const response = await fetch("/api/items/1");
if (!response.ok) {
throw await HttpError.from(response);
}Below are some examples of how to use this module.
This class can be used on its own to create any HttpError. It has a few
different call signatures you can use. The 4 examples below would throw the same
error.
import { HttpError } from "@udibo/http-error";
throw new HttpError(404, "file not found");
throw new HttpError(404, { message: "file not found" });
throw new HttpError("file not found", { status: 404 });
throw new HttpError({ status: 404, message: "file not found" });You can also include a cause in the optional options argument for it like you
can with regular errors. Additional HttpErrorOptions include statusText,
type (a URI for the problem type), instance (a URI for this specific error
occurrence), extensions (an object for additional details), and headers (to
customize response headers).
import { HttpError, type HttpErrorOptions } from "@udibo/http-error";
const cause = new Error("Underlying issue");
throw new HttpError(400, "Invalid input", {
cause,
type: "/errors/validation-error",
instance: "/requests/123/user-field",
extensions: { field: "username", reason: "must be alphanumeric" },
headers: { "X-Custom-Error-ID": "err-987" },
});All HttpError objects have a status associated with them. If a status is not
provided it will default to 500. The expose property will default to true
for client error statuses (4xx) and false for server error statuses (5xx). You
can override the default behavior by setting the expose property on the
options argument.
For all known HTTP error status codes, a name will be generated (e.g.,
Not Found for 404). If the name is not known, it will default to
Unknown Client Error or Unknown Server Error.
import { HttpError } from "@udibo/http-error";
const error = new HttpError(404, "file not found");
console.log(error.toString()); // Not Found: file not found
console.log(error.status); // 404
console.log(error.expose); // trueIf you would like to extend the HttpError class, you can pass your own error
name in the options.
import { HttpError, type HttpErrorOptions } from "@udibo/http-error";
class CustomError extends HttpError {
constructor(message?: string, options?: HttpErrorOptions) {
super(message, { name: "CustomError", status: 420, ...options });
}
}This static method intelligently converts various error-like inputs into an
HttpError instance.
- If an
HttpErroris passed, it's returned directly. - If an
Erroris passed, it's wrapped in a newHttpError(usually 500 status), with the original error as thecause. - If a
Responseobject is passed, it attempts to parse the body as RFC 9457 Problem Details. If successful, it creates anHttpErrorfrom those details. If parsing fails or the body isn't Problem Details, it creates a genericHttpErrorbased on the response status. This method is asynchronous and returns aPromise<HttpError>. - If a
ProblemDetailsobject is passed, it creates anHttpErrorfrom it. - For other unknown inputs, it creates a generic
HttpErrorwith a 500 status.
import { HttpError } from "@udibo/http-error";
// From a standard Error
const plainError = new Error("Something went wrong");
const httpErrorFromError = HttpError.from(plainError);
console.log(httpErrorFromError.status); // 500
console.log(httpErrorFromError.message); // "Something went wrong"
console.log(httpErrorFromError.cause === plainError); // true
// From a Response (example)
async function handleErrorResponse(response: Response) {
if (!response.ok) {
const error = await HttpError.from(response);
// Now 'error' is an HttpError instance
throw error;
}
return response.json();
}
// From a ProblemDetails object
const problemDetails = {
status: 403,
title: "ForbiddenAccess",
detail: "You do not have permission.",
type: "/errors/forbidden",
};
const httpErrorFromDetails = HttpError.from(problemDetails);
console.log(httpErrorFromDetails.status); // 403
console.log(httpErrorFromDetails.name); // ForbiddenAccessThis method returns a plain JavaScript object representing the error in the RFC
9457 Problem Details format. This is useful for serializing the error to a JSON
response body. The detail property is set to exposedMessage, which provides
a safe message for clients (see exposedMessage section below).
import { HttpError } from "@udibo/http-error";
const error = new HttpError(400, "Invalid input", {
type: "/errors/validation",
instance: "/form/user",
extensions: { field: "email" },
});
const problemDetails = error.toJSON();
console.log(problemDetails);
// Outputs:
// {
// field: "email",
// status: 400,
// title: "Bad Request",
// detail: "Invalid input",
// type: "/errors/validation",
// instance: "/form/user"
// }
// For server errors (expose=false), detail uses a safe default message
const serverError = new HttpError(500, "SQL syntax error near 'users'");
console.log(serverError.toJSON());
// Outputs:
// {
// status: 500,
// title: "Internal Server Error",
// detail: "The server encountered an unexpected condition."
// }This method returns a Response object, ready to be sent to the client. The
response body will be the JSON string of the Problem Details object (from
toJSON()), and headers (including Content-Type: application/problem+json)
will be set. The response status and status text will match the error's
properties.
import { HttpError } from "@udibo/http-error";
const error = new HttpError(401, "Authentication required");
const response = error.getResponse();
console.log(response.status); // 401
console.log(response.headers.get("Content-Type")); // application/problem+json
// response.body can be read to get the JSON stringThe exposedMessage property provides a safe, user-friendly message that can be
exposed to clients. This prevents internal error details (like SQL errors, file
paths, or stack traces) from leaking to users.
How it works:
- If
exposedMessageis explicitly provided in options, that value is used - If
exposeistrueandmessageexists,exposedMessagedefaults tomessage - Otherwise,
exposedMessagedefaults to a generic message for the status code (e.g., "The server encountered an unexpected condition." for 500)
import { HttpError } from "@udibo/http-error";
// Client error (expose=true by default) - message is used as exposedMessage
const clientError = new HttpError(400, "Invalid email format");
console.log(clientError.exposedMessage); // "Invalid email format"
// Server error (expose=false by default) - safe default is used
const serverError = new HttpError(500, "Database connection refused");
console.log(serverError.exposedMessage); // "The server encountered an unexpected condition."
console.log(serverError.message); // "Database connection refused" (internal use only)
// Custom exposedMessage - always takes priority
const customError = new HttpError(500, "SQL syntax error", {
exposedMessage: "An error occurred while processing your request.",
});
console.log(customError.exposedMessage); // "An error occurred while processing your request."When rendering error messages in server-side templates or responses, always use
error.exposedMessage instead of error.message to prevent leaking internal
implementation details to users.
Why this matters:
error.messagemay contain sensitive information (SQL errors, file paths, stack traces)error.exposedMessageprovides a safe, user-friendly message by default- For server errors (5xx), the default
exposedMessageis generic and safe - For client errors (4xx),
exposedMessagedefaults tomessagesince those are typically user-facing
Example in a template:
// BAD - may expose internal details
<p>Error: ${error.message}</p> // "SQLSTATE[42S02]: Table 'users' doesn't exist"
// GOOD - safe for users
<p>Error: ${error.exposedMessage}</p> // "The server encountered an unexpected condition."Example in error handling middleware:
app.onError((cause, c) => {
const error = HttpError.from(cause);
// Log the full internal message for debugging
console.error(`[${error.status}] ${error.message}`, error.cause);
// Return safe message to client (via toJSON which uses exposedMessage)
return error.getResponse();
});This factory function allows you to create custom error classes that extend
HttpError. You can provide default options (like status, name, message,
extensions, etc.) for your custom error class.
import { createHttpErrorClass, type HttpErrorOptions } from "@udibo/http-error";
interface MyApiErrorExtensions {
errorCode: string;
requestId?: string;
}
const MyApiError = createHttpErrorClass<MyApiErrorExtensions>({
name: "MyApiError", // Default name
status: 452, // Default status
extensions: {
errorCode: "API_GENERAL_FAILURE", // Default extension value
},
});
try {
throw new MyApiError("Specific operation failed.", {
extensions: {
errorCode: "API_OP_X_FAILED", // Override default extension
requestId: "req-123", // Add instance-specific extension
},
});
} catch (e) {
if (e instanceof MyApiError) {
console.log(e.name); // "MyApiError"
console.log(e.status); // 452
console.log(e.message); // "Specific operation failed."
console.log(e.extensions.errorCode); // "API_OP_X_FAILED"
console.log(e.extensions.requestId); // "req-123"
// console.log(e.toJSON());
}
}
// Instance with overridden status
const anotherError = new MyApiError(453, "Another failure");
console.log(anotherError.status); // 453
console.log(anotherError.extensions.errorCode); // "API_GENERAL_FAILURE" (from default)Here's an example of how to use HttpError with Hono, utilizing its
app.onError handler to return Problem Details JSON responses. It is also
available as examples/hono.ts, with tests in
examples/hono.test.ts that call the app with
app.request().
import { Hono } from "hono";
import { HTTPException } from "hono/http-exception";
import { HttpError } from "@udibo/http-error";
const app = new Hono();
app.get("/error", () => {
throw new Error("This is an example of a plain Error");
});
app.get("/hono-error", () => {
throw new HTTPException(400, {
message: "This is an example of an error from hono",
});
});
app.get("/http-error", () => {
throw new HttpError(400, "This is an example of an HttpError", {
type: "/errors/http-error",
instance: "/errors/http-error/instance/123",
extensions: {
customField: "customValue",
},
});
});
app.onError((cause) => {
// Converts non-HttpError instances to HttpError instances
const error = HttpError.from(cause);
// Log the full internal error, including the cause
console.error(error);
// The response body only contains the safe `exposedMessage`
return error.getResponse();
});
export default app;To run it on Node.js, serve the app with
@hono/node-server:
import { serve } from "@hono/node-server";
import app from "./app.ts";
serve({ fetch: app.fetch, port: 8000 });