Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Typed Errors - Zagora
Skip to content

Typed Errors

Define structured error responses

Zagora lets you define error schemas upfront, giving you strongly-typed error helpers inside handlers and typed error responses for consumers.

Defining Errors

Use the .errors() method with an object mapping error kinds to schemas:

import { z } from 'zod';
import { zagora } from 'zagora';
 
const getUser = zagora()
  .input(z.string())
  .errors({
    NOT_FOUND: z.object({
      userId: z.string(),
      message: z.string()
    }),
    UNAUTHORIZED: z.object({
      requiredRole: z.string()
    })
  })
  .handler(({ errors }, userId) => {
    if (!currentUser) {
      throw errors.UNAUTHORIZED({ requiredRole: 'user' });
    }
    
    const user = db.find(userId);
    if (!user) {
      throw errors.NOT_FOUND({ 
        userId, 
        message: 'User not found' 
      });
    }
    
    return user;
  })
  .callable();

Error Keys Must Be Uppercase

Error keys represent error "kinds" and must be UPPERCASE:

// Good
.errors({
  NOT_FOUND: z.object({ id: z.string() }),
  RATE_LIMITED: z.object({ retryAfter: z.number() })
})
 
// Bad - TypeScript will error
.errors({
  notFound: z.object({ id: z.string() })  // Must be uppercase!
})

Using Error Helpers

Inside the handler, errors provides typed helper functions:

.handler(({ errors }, input) => {
  // errors.NOT_FOUND() - creates NOT_FOUND error
  // errors.UNAUTHORIZED() - creates UNAUTHORIZED error
  
  throw errors.NOT_FOUND({ userId: '123', message: 'Gone' });
})

The helper validates the error payload at runtime:

// This will cause a VALIDATION_ERROR because retryAfter should be number
throw errors.RATE_LIMITED({ retryAfter: 'invalid' });

Consuming Typed Errors

Consumers can narrow error types using the kind property:

const result = getUser('123');
 
if (!result.ok) {
  switch (result.error.kind) {
    case 'NOT_FOUND':
      console.log(result.error.userId);   // string
      console.log(result.error.message);  // string
      break;
    case 'UNAUTHORIZED':
      console.log(result.error.requiredRole);  // string
      break;
    case 'VALIDATION_ERROR':
      console.log(result.error.issues);  // Schema.Issue[]
      break;
    case 'UNKNOWN_ERROR':
      console.log(result.error.cause);  // unknown
      break;
  }
}

Built-in Error Kinds

Zagora includes two built-in error kinds:

VALIDATION_ERROR

Returned when input, output, or error payload validation fails:

{
  kind: 'VALIDATION_ERROR',
  message: 'Validation failed',
  issues: [{ path: ['email'], message: 'Invalid email' }],
  key?: 'NOT_FOUND'  // Present if error helper validation failed
}

UNKNOWN_ERROR

Returned when an untyped error is thrown:

.handler(() => {
  throw new Error('Something went wrong');
})
 
// Results in:
{
  kind: 'UNKNOWN_ERROR',
  message: 'Something went wrong',
  cause: Error('Something went wrong')
}

Error Validation

Error payloads are validated at runtime. Invalid payloads become VALIDATION_ERROR:

const proc = zagora()
  .errors({
    RATE_LIMITED: z.object({ retryAfter: z.number() })
  })
  .handler(({ errors }) => {
    // Wrong type for retryAfter
    throw errors.RATE_LIMITED({ retryAfter: 'soon' });
  })
  .callable();
 
const result = proc();
// result.error.kind === 'VALIDATION_ERROR'
// result.error.key === 'RATE_LIMITED'

Strict Error Schemas

Use .strict() to reject unknown properties:

.errors({
  NOT_FOUND: z.object({ id: z.string() }).strict()
})
 
// This will fail validation:
throw errors.NOT_FOUND({ id: '123', extra: 'field' });

Next Steps