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
- Error Type Guards - Narrow error types with utilities
- Never-Throwing Guarantees - How errors are always captured