Error handling patterns.
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 SYS_INTERNAL_001 / 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.
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.
apps/backend/src/common/errors/error-codes.ts.apps/backend/src/common/errors/vendix-http.exception.ts.apps/backend/src/common/filters/http-exception.filter.ts.apps/frontend/src/app/core/utils/error-messages.ts.apps/frontend/src/app/core/utils/parse-api-error.ts and api-error-handler.ts.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.
Responses include:
statusCodeerror_codemessagetimestamppathdetailsdevDetailsValidation arrays are mapped by AllExceptionsFilter to SYS_VALIDATION_001. Unknown errors map to SYS_INTERNAL_001.
Use extractApiErrorMessage(error) for simple display. It delegates to parseApiError() when error_code exists and maps to ERROR_MESSAGES.
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.
error-codes.ts near the owning domain.VendixHttpException at the service/controller boundary.error-messages.ts if the error can reach UI.details safe for clients; put sensitive diagnostics only in logs/dev details.vendix-validationvendix-backend-apivendix-frontend