export abstract class HttpError extends Error { abstract readonly status: number; abstract readonly code: string; constructor(message: string, readonly details?: unknown) { super(message); this.name = new.target.name; } toJSON() { return { error: { code: this.code, message: this.message, details: this.details, }, }; } } export class BadRequestError extends HttpError { readonly status = 400; readonly code = "BAD_REQUEST"; } export class UnauthorizedError extends HttpError { readonly status = 401; readonly code = "UNAUTHORIZED"; } export class ForbiddenError extends HttpError { readonly status = 403; readonly code = "FORBIDDEN"; } export class NotFoundError extends HttpError { readonly status = 404; readonly code = "NOT_FOUND"; } export class ConflictError extends HttpError { readonly status = 409; readonly code = "CONFLICT"; } export class InternalError extends HttpError { readonly status = 500; readonly code = "INTERNAL_ERROR"; } interface ElysiaErrorContext { code: string | number; error: unknown; set: { status?: number | string }; } /** * Drop-in handler for Elysia's `.onError()`. Maps our HttpError hierarchy, * Elysia's built-in error codes, and anything unrecognized to a consistent * `{ error: { code, message, details? } }` JSON body. */ export function toErrorResponse({ code, error, set }: ElysiaErrorContext) { if (error instanceof HttpError) { set.status = error.status; return error.toJSON(); } if (code === "VALIDATION") { set.status = 422; return { error: { code: "VALIDATION", message: error instanceof Error ? error.message : "validation failed", }, }; } if (code === "NOT_FOUND") { set.status = 404; return { error: { code: "NOT_FOUND", message: "not found" } }; } set.status = 500; return { error: { code: "INTERNAL_ERROR", message: error instanceof Error ? error.message : "internal server error", }, }; }