Backend Error Handling
Compass uses typed operational errors plus a centralized Express error handler.
Principles
- Minimize the number of
try/catchblocks in the code.
Source Files
packages/backend/src/common/errors/handlers/error.handler.tspackages/backend/src/common/errors/handlers/error.express.handler.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 turn it into the client payload
Example:
import { AuthError } from "@backend/common/errors/auth/auth.errors";
import { error } from "@backend/common/errors/handlers/error.handler";
throw error(AuthError.MissingRefreshToken, "Google connection required");
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
Internal details such as stack traces and operational flags stay server-side.
Unexpected Error Rules
- non-
BaseErrorvalues are routed throughhandleExpressError(...) - Google API errors get special handling for revoked tokens, invalid values, and full-sync recovery
- programmer errors can terminate the process after logging
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(...).