Result<T, E> — a discriminated union that makes success and failure explicit in the type system. You handle errors by matching on them, not by wrapping code in try-catch.
Why errors as values?
Exceptions are invisible in TypeScript. A function signaturegetUser(id: string): User tells you nothing about what can go wrong. The caller has no idea they need to handle NotFoundError or UniqueViolationError until it blows up at runtime.
With Result types, failures are visible in the signature:
The Result type
Creating results
Handling results
Pattern matching
Exhaustive error matching
matchErr requires a handler for every error code in the union. Miss one and the compiler tells you:
Transforming results
Unwrapping (tests and scripts only)
Domain errors
Errors are organized by domain — each boundary has its own error vocabulary.Database errors
Returned bydb.* operations:
Client errors
Returned by the generated SDK on the client side. These use a simpler vocabulary — no database internals leak to the frontend:Validation errors
Returned by schema validation (safeParse):
Error boundaries
Errors cross three boundaries in a Vertz app. At each boundary, errors are translated to the appropriate vocabulary:Database → Server
Database errors map to HTTP status codes automatically:
The entity framework handles this mapping — you don’t write it manually.
Server → Client
HTTP responses map to client error types:
The generated SDK handles this mapping — the client receives typed
Result<T, ApiError> values.
Why translate?
A databaseUNIQUE_VIOLATION on column email with constraint users_email_key is an implementation detail. The client doesn’t need to know about constraint names or column internals. It needs to know: “there’s a conflict on the email field.” The boundary mappings strip internal details and produce domain-appropriate errors at each layer.
Custom domain errors
For application-specific errors, extendAppError:
AppError provides:
- A typed
codefield for pattern matching toJSON()for automatic HTTP serialization- Consistent structure across all custom errors
Infrastructure errors
Not all errors are domain errors. Infrastructure failures — database connection lost, request timeout, pool exhausted — are truly exceptional. These do throw, and are caught by global middleware that returns a 503:
You don’t handle these in business logic. They’re caught at the top level and produce appropriate error responses automatically.
The rule
Expected failures are values. Unexpected failures are exceptions.- Record not found? Expected — return
Result. - Unique constraint? Expected — return
Result. - Validation failed? Expected — return
Result. - Database crashed? Unexpected — throw, let middleware handle it.