Circle V2 API Docs
    Preparing search index...

    Error Handling

    Error handling is built on two packages: @repo/errors for the error hierarchy and @repo/safe for the result type.

    All application errors extend CircleError, defined in packages/errors/src/index.ts:

    export abstract class CircleError<Code extends string = string, Domain extends string = string> extends Error {
    abstract readonly code: Code;
    abstract readonly domain: Domain;
    readonly context: Record<string, unknown> = {};

    with(ctx: Record<string, unknown>): this {
    Object.assign(this.context, ctx);
    return this;
    }

    toJSON(): Record<string, unknown> {
    return { code: this.code, name: this.name, message: this.message, context: this.context };
    }
    }
    Error Code Package Usage
    UnhandledError UNHANDLED_ERROR @repo/errors Catch-all for unexpected errors
    ValidationError VALIDATION_ERROR @repo/errors Input validation failures
    EnvironmentVariableNotSetError ENVIRONMENT_VARIABLE_NOT_SET_ERROR @repo/errors Missing env var
    DbNotFoundError DB_NOT_FOUND_ERROR @repo/db Record not found in DB
    DbConnectionError DB_CONNECTION_ERROR @repo/db Database connection failure
    UnhandledPostgresError UNHANDLED_POSTGRES_ERROR @repo/db Unexpected PostgreSQL error
    // Good
    import { ValidationError } from "@repo/errors";
    throw new ValidationError("Invalid input");

    // Bad
    throw new Error("Invalid input");
    // Good -- static message, dynamic context attached separately
    throw new UnhandledError("Patient not found").with({ patientId });

    // Bad -- dynamic string interpolation in message
    throw new UnhandledError(`Patient not found: ${patientId}`);

    This convention keeps error messages greppable and avoids accidentally leaking sensitive data into messages.

    Never attach passwords, names, addresses, phone numbers, or other PII to error context.

    The Safe<T> type from @repo/safe is a Go-inspired result type for explicit error handling:

    type Safe<T> = SafeSuccess<T> | SafeError;

    type SafeSuccess<T> = { data: T; error: null };
    type SafeError = { data: null; error: CircleError | UnhandledError };
    import { safe } from "@repo/safe";

    // Good -- explicit result handling
    const { data, error } = await safe(async () => await fetchPatients());

    if (error) {
    // handle error
    return;
    }

    // data is typed and non-null here

    // Bad -- implicit exception handling
    try {
    const data = await fetchPatients();
    } catch (error) {
    // ...
    }

    When you want throw-on-error semantics (e.g. in auth code where failure should halt execution):

    import { unwrap } from "@repo/safe";

    const user = await unwrap(userRepo.getById(userId));
    // throws if the repo returned an error
    Function Purpose
    safe(fn, ...args) Wraps a function call, returns Safe<T>
    unwrap(result) Extracts data or throws error
    safeSuccess(data) Creates a SafeSuccess<T>
    safeError(error) Creates a SafeError (normalizes non-CircleError inputs)
    %%{init:{"theme":"dark"}}%% graph TD A["Repo method throws<br/>(e.g. executeTakeFirstOrThrow)"] --> B["createDbRepo catches"] B --> C{"Error type?"} C -->|NoResultError| D["DbNotFoundError"] C -->|Connection error| E["DbConnectionError<br/>(re-thrown, not Safe)"] C -->|PostgrestError| F["UnhandledPostgresError"] C -->|Other| G["UnhandledError"] D --> H["Safe error returned"] F --> H G --> H H --> I["tRPC procedure checks error"] I --> J["Maps to TRPCError<br/>(NOT_FOUND, BAD_REQUEST, etc.)"] J --> K["Client receives typed error"]
    %%{init:{"theme":"default"}}%% graph TD A["Repo method throws<br/>(e.g. executeTakeFirstOrThrow)"] --> B["createDbRepo catches"] B --> C{"Error type?"} C -->|NoResultError| D["DbNotFoundError"] C -->|Connection error| E["DbConnectionError<br/>(re-thrown, not Safe)"] C -->|PostgrestError| F["UnhandledPostgresError"] C -->|Other| G["UnhandledError"] D --> H["Safe error returned"] F --> H G --> H H --> I["tRPC procedure checks error"] I --> J["Maps to TRPCError<br/>(NOT_FOUND, BAD_REQUEST, etc.)"] J --> K["Client receives typed error"]
    graph TD
      A["Repo method throws<br/>(e.g. executeTakeFirstOrThrow)"] --> B["createDbRepo catches"]
      B --> C{"Error type?"}
      C -->|NoResultError| D["DbNotFoundError"]
      C -->|Connection error| E["DbConnectionError<br/>(re-thrown, not Safe)"]
      C -->|PostgrestError| F["UnhandledPostgresError"]
      C -->|Other| G["UnhandledError"]
      D --> H["Safe error returned"]
      F --> H
      G --> H
      H --> I["tRPC procedure checks error"]
      I --> J["Maps to TRPCError<br/>(NOT_FOUND, BAD_REQUEST, etc.)"]
      J --> K["Client receives typed error"]

    1. Repo method -- throws if not found:

    // packages/db/src/repos/patients.repo.ts
    getById: async (id: PatientId) => {
    return await db.selectFrom("patient").selectAll()
    .where("patient_id", "=", id)
    .executeTakeFirstOrThrow(); // throws NoResultError if no row
    },

    2. createDbRepo catches and maps to Safe:

    // Internally, createDbRepo wraps this to:
    // { data: null, error: DbNotFoundError }

    3. tRPC procedure checks the result:

    const { data: patient, error } = await patientsRepo.getById(input.id);

    if (error) {
    throw new TRPCError({ code: "NOT_FOUND", message: "Patient not found" });
    }

    return patient;

    4. Client receives a typed tRPC error that React Query surfaces as error in the hook.

    To add a new domain error:

    import { CircleError } from "@repo/errors";

    export class MyDomainError extends CircleError {
    readonly code = "MY_DOMAIN_ERROR" as const;
    readonly domain = "my-domain" as const;
    }

    Place domain-specific errors in the relevant package (e.g. DB errors in packages/db/src/errors.ts).


    Next: UI and Components