SKILL.md
Vendix Error Handling
Hard Rule — Rethrow, Never Return An Error
AllExceptionsFilter runs only when the exception leaves the handler. Returning responseService.error(...) from a catch consumes it, so Nest answers HTTP 200 with the real status buried in the body. Any client that reads the status line sees a success where the backend rejected.
// WRONG — responds 200 with {"success":false,…,"statusCode":409}
try {
const result = await this.tablesService.update(id, dto);
return this.responseService.updated(result, 'Mesa actualizada exitosamente');
} catch (error) {
return this.responseService.error(error.message, error.status || 400);
}
// RIGHT — no try/catch; the service throws typed and the filter emits 409 + error_code
const result = await this.tablesService.update(id, dto);
return this.responseService.updated(result, 'Mesa actualizada exitosamente');
Precondition before deleting a try/catch: every path of the service must throw VendixHttpException or a Nest HttpException. Verify with grep -c "throw new Error" <service> — it must be 0, otherwise the filter degrades the case to SYSINTERNAL001 / 500, which is worse than the 200.
Reference implementations with zero try/catch: apps/backend/src/domains/store/tables/tables.controller.ts and its sibling table-sessions.controller.ts.
Measured 2026-07-30: 358 responseService.error calls across 54 controllers still carry the pattern, plus 16 frontend reads of success === false compensating for it. Repo-wide sweep ticket: QUI-571.
Deliberate exception — do not "fix" it. apps/backend/src/domains/auth/auth.controller.ts answers 200 with statusCode: 401 on failed login, and apps/frontend/src/app/core/store/auth/auth.effects.ts:193,488 reads that body on purpose. Changing it breaks login.
Frontend corollary
Never hand-roll an envelope unwrapper (if (res.success === false) throw ...). It hides a broken contract instead of reporting it, and the filter's error body carries "success": null — not false — so the check silently stops matching the moment the status starts travelling correctly. Rely on catchError + extractApiErrorMessage.
Source of Truth
- Backend codes:
apps/backend/src/common/errors/error-codes.ts. - Exception class:
apps/backend/src/common/errors/vendix-http.exception.ts. - Global filter:
apps/backend/src/common/filters/http-exception.filter.ts. - Frontend messages:
apps/frontend/src/app/core/utils/error-messages.ts. - Frontend parser:
apps/frontend/src/app/core/utils/parse-api-error.tsandapi-error-handler.ts.
Backend Pattern
Prefer VendixHttpException with an existing ErrorCodes entry:
throw new VendixHttpException(ErrorCodes.PAYMENT_SOURCE_NOT_FOUND, undefined, { payment_source_id });
The registry contains mixed naming styles. Do not invent a stricter format than the current file; follow nearby domain naming.
Response Shape
Responses include:
statusCodeerror_codemessagetimestamppath- optional
details - optional non-production
devDetails
Validation arrays are mapped by AllExceptionsFilter to SYSVALIDATION001. Unknown errors map to SYSINTERNAL001.
Frontend Pattern
Use extractApiErrorMessage(error) for simple display. It delegates to parseApiError() when errorcode exists and maps to ERRORMESSAGES.
Use parseApiError() directly only when component behavior depends on the code:
const { errorCode, userMessage } = parseApiError(error);
this.toastService.error(userMessage);
Never display backend developer details to users.
Adding A New Error
- Add the backend code in
error-codes.tsnear the owning domain. - Use
VendixHttpExceptionat the service/controller boundary. - Add or update frontend UX copy in
error-messages.tsif the error can reach UI. - Keep
detailssafe for clients; put sensitive diagnostics only in logs/dev details.
Related Skills
vendix-validationvendix-backend-apivendix-frontend