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

Async Support

Sync and async procedure inference

Zagora dynamically infers whether a procedure is sync or async based on your handler and schemas.

Automatic Inference

Sync Handler = Sync Result

import { z } from 'zod';
import { zagora } from 'zagora';
 
const add = zagora()
  .input(z.tuple([z.number(), z.number()]))
  .handler((_, a, b) => a + b)  // Sync handler
  .callable();
 
const result = add(1, 2);  // ZagoraResult<number> (not Promise!)
console.log(result.data);  // 3

Async Handler = Async Result

const fetchUser = zagora()
  .input(z.string())
  .handler(async (_, id) => {  // Async handler
    const res = await fetch(`/api/users/${id}`);
    return res.json();
  })
  .callable();
 
const result = await fetchUser('123');  // Promise<ZagoraResult<User>>
console.log(result.data);

Why This Matters

Unlike oRPC/tRPC where everything is async, Zagora preserves synchronicity:

// oRPC/tRPC - always async, even for sync operations
const result = await add({ a: 1, b: 2 });
 
// Zagora - sync when possible
const result = add(1, 2);  // No await needed!

This is crucial for:

  • Library APIs that should be sync
  • Performance-sensitive code
  • Simpler call sites

Async Schemas

If your schema has async validation, the procedure becomes async:

const createUser = zagora()
  .input(z.object({
    email: z.string().refine(
      async (email) => await isUniqueEmail(email),
      'Email already exists'
    )
  }))
  .handler((_, input) => input)  // Sync handler
  .callable();
 
// Must await due to async schema validation
const result = await createUser({ email: 'test@example.com' });

Async Cache

If your cache adapter has async methods, the procedure becomes async:

const proc = zagora()
  .cache(redisCache)  // has async .has() and .get()
  .input(z.string())
  .handler((_, input) => expensiveSync(input))
  .callable();
 
// Must await due to async cache
const result = await proc('key');

Mixed Sync/Async

You can have both sync and async procedures in the same codebase:

// Sync - for simple operations
const validate = zagora()
  .input(z.string().email())
  .handler((_, email) => ({ valid: true, email }))
  .callable();
 
// Async - for I/O operations
const sendEmail = zagora()
  .input(z.object({ to: z.string(), body: z.string() }))
  .handler(async (_, { to, body }) => {
    await emailService.send(to, body);
    return { sent: true };
  })
  .callable();
 
// Use them appropriately
const validation = validate('test@example.com');  // Sync
const sent = await sendEmail({ to: 'test@example.com', body: 'Hi' });  // Async

Promise Return Type

If your handler returns a Promise explicitly, it's async:

const fetchData = zagora()
  .input(z.string())
  .handler((_, url) => {
    return fetch(url).then(r => r.json());  // Returns Promise
  })
  .callable();
 
const result = await fetchData('/api');

Type Inference

TypeScript correctly infers the result type:

const syncProc = zagora()
  .input(z.number())
  .handler((_, n) => n * 2)
  .callable();
 
const asyncProc = zagora()
  .input(z.number())
  .handler(async (_, n) => n * 2)
  .callable();
 
// TypeScript infers:
type SyncResult = ReturnType<typeof syncProc>;    // ZagoraResult<number>
type AsyncResult = ReturnType<typeof asyncProc>;  // Promise<ZagoraResult<number>>

Next Steps