Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
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 (uncaught excception, TypeError, SyntaxError, anything inside the handler):

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

NOTE: Currently, when there is a failure fired in Cache Adapter methods it throws that error kind too, but soon it will throw it's own kind.

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