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

Never-Throwing Guarantees

Procedures never crash your process

Zagora procedures are guaranteed to never throw exceptions. Every outcome—success or failure—is returned as a structured result.

The Guarantee

No matter what happens inside a procedure, you always get a ZagoraResult:

import { z } from 'zod';
import { zagora } from 'zagora';
 
const riskyOperation = zagora()
  .input(z.string())
  .handler((_, input) => {
    throw new Error('Something went wrong!');
  })
  .callable();
 
// This does NOT throw - it returns an error result
const result = riskyOperation('test');
 
console.log(result.ok);           // false
console.log(result.error.kind);   // 'UNKNOWN_ERROR'
console.log(result.error.cause);  // Error: Something went wrong!

What Gets Captured

Handler Exceptions

Any error thrown in the handler becomes an UNKNOWN_ERROR:

.handler(() => {
  throw new Error('Oops');
})
// => { ok: false, error: { kind: 'UNKNOWN_ERROR', message: 'Oops', cause: Error } }

Validation Failures

Invalid inputs become VALIDATION_ERROR:

const proc = zagora()
  .input(z.number())
  .handler((_, n) => n * 2)
  .callable();
 
proc('not a number');
// => { ok: false, error: { kind: 'VALIDATION_ERROR', issues: [...] } }

Typed Errors

Your defined errors are captured and typed:

const proc = zagora()
  .errors({ NOT_FOUND: z.object({ id: z.string() }) })
  .handler(({ errors }) => {
    throw errors.NOT_FOUND({ id: '123' });
  })
  .callable();
 
const result = proc();
// => { ok: false, error: { kind: 'NOT_FOUND', id: '123' } }

Syntax Errors

Even syntax errors are captured:

.handler(() => {
  const obj = null;
  return obj.property;  // TypeError: Cannot read property of null
})
// => { ok: false, error: { kind: 'UNKNOWN_ERROR', cause: TypeError } }

Async Rejections

Promise rejections are captured:

.handler(async () => {
  const response = await fetch('/failing-endpoint');
  if (!response.ok) throw new Error('Request failed');
})
// => { ok: false, error: { kind: 'UNKNOWN_ERROR', ... } }

Result Structure

Success

{
  ok: true,
  data: T,          // Your return value
  error: undefined
}

Failure

{
  ok: false,
  data: undefined,
  error: {
    kind: 'VALIDATION_ERROR' | 'UNKNOWN_ERROR' | YourErrorKinds,
    message: string,
    // ... additional fields based on error kind
  }
}

Why Never-Throwing?

Predictable Control Flow

// Traditional - must remember to try/catch
try {
  const user = getUser(id);
  processUser(user);
} catch (e) {
  // What type is e? Unknown!
  handleError(e);
}
 
// Zagora - explicit error handling
const result = getUser(id);
if (result.ok) {
  processUser(result.data);
} else {
  // result.error is fully typed
  handleError(result.error);
}

No Accidental Crashes

// Traditional - unhandled rejection can crash process
await riskyAsyncOperation();  // Might throw!
 
// Zagora - always safe
const result = await riskyAsyncOperation();  // Never throws
if (!result.ok) {
  console.error('Failed:', result.error);
}

Typed Errors

if (!result.ok) {
  switch (result.error.kind) {
    case 'NOT_FOUND':
      // TypeScript knows the shape
      console.log(result.error.id);
      break;
    case 'VALIDATION_ERROR':
      console.log(result.error.issues);
      break;
    case 'UNKNOWN_ERROR':
      console.log(result.error.cause);
      break;
  }
}

Comparison with Other Approaches

vs try/catch

// try/catch - error type is unknown
try {
  const result = dangerousOperation();
} catch (e) {
  // e is `unknown` - must cast or check
}
 
// Zagora - error is fully typed
const result = safeOperation();
if (!result.ok) {
  result.error.kind;  // Typed!
}

vs neverthrow

// neverthrow - requires unwrapping
const result = await operation();
const value = result.unwrap();  // Throws if error!
 
// Zagora - direct access
const result = operation();
if (result.ok) {
  result.data;  // Direct access, no unwrap
}

vs Effect.ts

// Effect - complex API
const program = Effect.tryPromise({
  try: () => operation(),
  catch: (e) => new CustomError(e)
});
await Effect.runPromise(program);
 
// Zagora - simple
const result = operation();

Edge Cases

Cache Failures

const brokenCache = {
  has() { throw new Error('Cache failed'); },
  // ...
};
 
const result = proc();
// => { ok: false, error: { kind: 'UNKNOWN_ERROR', cause: Error('Cache failed') } }

Invalid Error Payloads

throw errors.NOT_FOUND({ wrongField: 'value' });
// => { ok: false, error: { kind: 'VALIDATION_ERROR', key: 'NOT_FOUND', ... } }

Next Steps