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
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., NotFoundError for 404). If the name is not known, it will default to UnknownClientError or UnknownServerError.
import { HttpError } from "@udibo/http-error"; const error = new HttpError(404, "file not found"); console.log(error.toString()); // NotFoundError: 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. If expose is false (default for 5xx errors), the detail (message) property will be omitted.
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: "BadRequestError", // detail: "Invalid input", // type: "/errors/validation", // instance: "/form/user" // } const serverError = new HttpError(500, "Internal details", { expose: false }); console.log(serverError.toJSON()); // Outputs (detail omitted): // { // status: 500, // title: "InternalServerError" // }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 stringThis 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.
import { Hono } from "hono"; import { HTTPException } from "hono/http-exception"; // For comparison 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", () => { // Hono's native error, for comparison 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", }, }); }); // Global error handler app.onError(async (cause, c) => { // c is Hono's context console.log("Hono onError caught:", cause); // Converts non-HttpError instances to HttpError instances // For Response objects (e.g. from fetch), HttpError.from is async const error = cause instanceof Response ? await HttpError.from(cause) : HttpError.from(cause); console.error(error); // Log the full HttpError return error.getResponse(); // Return a Response object directly }); console.log("Hono server running on http://localhost:8000"); Deno.serve(app.fetch);Here is an example of how an Oak server could have middleware that converts any thrown error into an HttpError and returns a Problem Details JSON response.
import { Application, Router } from "@oak/oak"; import { HttpError } from "@udibo/http-error"; const app = new Application(); // Error handling middleware app.use(async (context, next) => { try { await next(); } catch (cause) { // Converts non-HttpError instances to HttpError instances // For Response objects (e.g. from fetch), HttpError.from is async const error = cause instanceof Response ? await HttpError.from(cause) : HttpError.from(cause); console.error(error); // Log the full error const { response } = context; response.status = error.status; error.headers.forEach((value, key) => { // Set custom headers from HttpError response.headers.set(key, value); }); // Set Content-Type if not already set by error.headers if (!response.headers.has("Content-Type")) { response.headers.set("Content-Type", "application/problem+json"); } response.body = error.toJSON(); // Send Problem Details as JSON } }); const router = new Router(); router.get("/test-error", () => { // Will be caught and transformed by the middleware throw new Error("A generic error occurred!"); }); router.get("/test-http-error", () => { throw new HttpError(400, "This is a specific HttpError.", { type: "/errors/my-custom-error", extensions: { info: "some detail" }, }); }); app.use(router.routes()); app.use(router.allowedMethods()); console.log("Oak server running on http://localhost:8000"); await app.listen({ port: 8000 });