Backend Error Handling
Compass uses typed operational errors plus a centralized Express error handler.
Principles
- Minimize the number of
try/catchblocks in the code. - Never return non-HTTP statuses such as
Status.UNSURE(600) on live paths. - Event mutation routes use the
EventMutationErrorenvelope (code/message/retryable). Unknown/programmer errors are non-retryable 500 — only true Sync/provider failures stay retryablePROVIDER_FAILURE. - Sync proxy failures on calendar/auth reads use
throwSyncProxyFailure/unwrapSyncResult(503/502), neverGenericError.NotSure.
Source Files
packages/backend/src/common/errors/handlers/error.handler.tspackages/backend/src/common/errors/handlers/error.express.handler.tspackages/backend/src/common/services/sync-service/sync-proxy-error.tspackages/backend/src/event/event.error.ts- feature error metadata files under
packages/backend/src/common/errors/** packages/core/src/errors/errors.base.ts
Main Pattern
Preferred backend pattern:
- define reusable error metadata in the relevant feature file
- create a
BaseErrorthrougherror(...) - let controller/service code throw that error
- let centralized Express handling (
res.promise→handleExpressError) turn it into the client payload
Event mutation controllers may catch locally and call toEventMutationError
so the strict { code, message, retryable } envelope is preserved. User
controllers return JSON via toClientErrorPayload (or { code, message } for
unexpected errors) rather than empty bodies.
Example:
import { AuthError } from "@backend/common/errors/auth/auth.errors";
import { error } from "@backend/common/errors/handlers/error.handler";
throw error(AuthError.SyncConnectionUnavailable, "Could not reach sync");
Client Payload Rules
For BaseError, backend responses are intentionally small:
result: short result stringmessage: safe user-facing descriptioncode: optional stable machine-readable identifier for frontend branching
Event mutations use { code, message, retryable } instead.
Internal details such as stack traces and operational flags stay server-side.
Unexpected Error Rules
- non-
BaseErrorvalues are routed throughhandleExpressError(...)when usingres.promise - Sync client failures map to 502/503 (or typed mutation codes) — never HTTP 600
- programmer errors can terminate the process after logging when
isOperationalis false
Guidance
- Keep
resultshort and stable. - Add a
codewhen the frontend needs to branch on a specific operational error. - Keep
codestable and machine-oriented. Prefer values likeGOOGLE_ACCOUNT_ALREADY_CONNECTED. - Put technical detail in logs, not in the client payload.
- Prefer reusing existing feature error metadata before inventing new names.
- If the error should trigger special auth/sync behavior, verify both API handling and SSE side effects.
Shared Frontend-Backend Error Pattern
When a backend error needs typed frontend handling:
- add the backend error metadata in
packages/backend/src/common/errors/**with a stablecode - let
error(...)create theBaseError - expose the safe payload through the centralized handler in
packages/backend/src/common/errors/handlers/error.handler.ts - define the shared response schema in
packages/core/src/types - parse the error on the web side with a feature-specific wrapper around
parseApiError(...)
Example shape:
export const ApiErrorResponseSchema = z.object({
code: z.string(),
message: z.string().min(1),
});
export const GoogleConnectErrorResponseSchema = ApiErrorResponseSchema.extend({
code: z.enum([
"GOOGLE_ACCOUNT_ALREADY_CONNECTED",
"GOOGLE_CONNECT_EMAIL_MISMATCH",
]),
});
Keep shared parsing generic with parseApiError(error, schema). If a flow needs more than display-only fallback behavior, add a feature-specific parser such as parseGoogleConnectError(...) instead of baking one endpoint's schema into a generic helper name.
Adding New Backend Errors
When you need a new operational backend error:
- add metadata to the closest feature error file under
packages/backend/src/common/errors/** - throw it with
error(...)from service/controller code - log any extra debugging context before throwing
- if the web app needs typed handling, add or extend a shared schema in
packages/core/src/types - add frontend parsing through
parseApiError(...)plus a focused unit test
Example:
import { error } from "@backend/common/errors/handlers/error.handler";
import { UserError } from "@backend/common/errors/user/user.errors";
logger.error("Delete auth cleanup failed", { userId, summary, err });
throw error(UserError.DeleteCleanupFailed, "Delete Failed");
Do not introduce ad-hoc Error subclasses for operational backend failures. If
you need extra context like partial summaries, request payloads, or third-party
responses, log that context and still throw a standard BaseError via
error(...).