import { type ApiErrorResponse, ErrorCode } from "@batch-cooking/shared"; import { ZodError } from "zod"; import { HttpError } from "./http-error.js"; /** Return value of {@link ErrorHandlerService.handle}: everything a caller needs to send an HTTP response. */ export interface ErrorHandlingResult { /** HTTP status code to respond with. */ status: number; /** JSON body to respond with — matches the shared {@link ApiErrorResponse} contract. */ body: ApiErrorResponse; } /** * Centralizes every "how do we turn a thrown error into an HTTP response" * decision in one place, so route handlers and framework-specific * middleware (e.g. `createErrorMiddleware` in `@batch-cooking/express-tools`) * never duplicate this logic. Framework-agnostic on purpose — it only maps * an error to `{ status, body }` and has zero dependency on Express or any * other HTTP framework. * * Recognizes three error shapes today (zod validation failures, our own * `HttpError`, and anything else) and always falls back to a safe, generic * 500 for the unknown case — a caller of `handle()` never needs its own * fallback branch. */ export class ErrorHandlerService { /** * Maps any thrown value into a status + body pair ready to send to the * client. Always succeeds — an error that doesn't match a known shape * becomes a generic {@link ErrorCode.INTERNAL_ERROR} and is logged. */ public handle(error: unknown): ErrorHandlingResult { if (error instanceof ZodError) { return this._fromZodError(error); } if (error instanceof HttpError) { return this._fromHttpError(error); } return this._fromUnknownError(error); } /** Request body/query failed schema validation — always a 400. */ private _fromZodError(error: ZodError): ErrorHandlingResult { return { status: 400, body: { code: ErrorCode.VALIDATION_ERROR, message: "Validation error", details: error.flatten().fieldErrors, }, }; } /** Our own typed error — status/code were decided by whoever threw it. */ private _fromHttpError(error: HttpError): ErrorHandlingResult { return { status: error.status, body: { code: error.code, message: error.message }, }; } /** * Anything unrecognized: never leaks internal details to the client — * always a generic 500. Doesn't log the error itself — this class only * maps an error to `{ status, body }` (see the class doc comment); a * caller wanting this logged server-side does so on its own before/ * around calling `handle()` (see `apps/api`'s `errorLogger` middleware, * which sees every error — including this exact "unrecognized" case — * before it ever reaches here). */ private _fromUnknownError(_error: unknown): ErrorHandlingResult { return { status: 500, body: { code: ErrorCode.INTERNAL_ERROR, message: "Internal server error" }, }; } } /** Single shared instance — this service is stateless, no need for one per request. */ export const errorHandlerService = new ErrorHandlerService();