/** * @fileoverview Structured errors and Express helpers. * @module errors */ import { createDebugger } from './debug/logger.js'; import { getTraceId } from './debug/trace-context.js'; const log = createDebugger('app'); /** * Operational error with HTTP status and machine-readable code. */ export class AppError extends Error { /** * @param {string} message - Human-readable error. * @param {object} [options] * @param {number} [options.status=500] - HTTP status code. * @param {string} [options.code='INTERNAL_ERROR'] - Machine code. * @param {unknown} [options.details] - Optional structured details (safe for clients). */ constructor(message, { status = 500, code = 'INTERNAL_ERROR', details } = {}) { super(message); this.name = 'AppError'; this.status = status; this.code = code; this.details = details; } } /** * Wrap an async Express handler so rejected promises reach the error middleware. * * @param {(req: import('express').Request, res: import('express').Response, next: import('express').NextFunction) => Promise} fn * @returns {import('express').RequestHandler} */ export function asyncHandler(fn) { return (req, res, next) => { Promise.resolve(fn(req, res, next)).catch(next); }; } /** * Send a JSON error response. Logs server errors when debug is enabled. * * @param {import('express').Response} res * @param {Error|AppError} err * @returns {void} */ export function sendError(res, err) { const status = err instanceof AppError ? err.status : 500; const code = err instanceof AppError ? err.code : 'INTERNAL_ERROR'; const message = err?.message || 'Internal server error'; const details = err instanceof AppError ? err.details : undefined; const traceId = getTraceId(); if (status >= 500) { log.error('request failed', { status, code, message, traceId, stack: err?.stack, }); } else { log.debug('client error', { status, code, message, traceId }); } res.status(status).json({ error: message, code, ...(details !== undefined ? { details } : {}), ...(traceId ? { traceId } : {}), }); }