Async Programming

Zena provides built-in support for asynchronous programming using async functions, await expressions, and the Future<T> type. Async execution is cooperative and deterministic, compiling to state machines that run on a lightweight microtask queue.

The async/await model ​

An asynchronous function is marked with the async modifier:

zena
async function fetchUser(id: i32): Future<String> {
  let response = await fetch(`https://api.example.com/users/${id}`);
  return response.text();
}

Arrow functions and class methods can also be async:

zena
let loadConfig = async (path: String): Future<Config> => {
  let raw = await readFile(path);
  return parseConfig(raw);
};

class Service {
  async start(): Future<void> {
    await initializeDatabase();
  }
}

Return types and Future<T> ​

The return type of an async function is always Future<T>. Inside the function body:

  • A return expr; statement where expr has type T completes the returned future with that value.
  • Returning another future Future<T> forwards the result of that future without extra suspension overhead.
  • If no return type is annotated, the compiler infers Future<T> from the body's return expressions.

Future<T> is available globally from the prelude without explicit imports.

The await expression ​

The await operator pauses execution of the enclosing async function until the awaited future settles:

  • If the future resolves successfully, await produces the unpacked value of type T.
  • If the future fails, await throws the underlying error, which can be caught with a standard try/catch block.
zena
try {
  let user = await fetchUser(42);
  console.log(user);
} catch (e) {
  console.log(`Failed to fetch user: ${e.message}`);
}

await is valid only directly inside async function bodies.

Explicit suspension and cancellation points

Because Zena is statically typed, the compiler knows when an expression produces a Future<T> and could theoretically auto-suspend calls without await. However, Zena requires explicit await so that suspension boundaries remain clear and visible in source code. Every await is an asynchronous yielding point and a potential cancellation checkpoint.

Execution model: Eager start and concurrency ​

Zena's async execution follows an eager-start, single-threaded cooperative model.

Eager execution ​

Calling an async function begins executing its body synchronously and immediately until it encounters the first await on an unsettled future. At that point, it suspends and returns a pending Future<T> to the caller.

Concurrent execution ​

Because async functions start immediately upon invocation, concurrent operations can be started simply by invoking functions before awaiting their results:

zena
// Both network requests start immediately and run concurrently:
let futureA = fetchResource('/api/items');
let futureB = fetchResource('/api/pricing');

// Await both results when needed:
let items = await futureA;
let pricing = await futureB;

Run-to-completion guarantees ​

Between await suspension points, synchronous code runs to completion without preemption. There are no background thread context switches or shared-memory data races within an async task.

Continuations resume in deterministic order from a FIFO microtask queue managed by the Zena runtime.

Async main ​

Zena allows the top-level main() entry point of a program to be async:

zena
export async function main(): Future<i32> {
  console.log('Starting application...');
  let config = await loadConfig('config.json');
  await runServer(config);
  return 0;
}

When main() is asynchronous, the runtime executes the initial synchronous segment, enters the event loop, and drains the microtask queue until all pending tasks complete. If unhandled futures remain unsettled with no active I/O or timers, the runtime reports a deadlock error.

Futures and combinators ​

The standard library provides utilities for constructing, combining, and bridging futures.

Immediate futures ​

Create already-settled futures with Future.of and Future.failed:

zena
let success = Future.of(42); // Future<i32> already resolved
let failure = Future.failed<String>(new Error('Not found'));

Concurrent combinators ​

Use Future.all and Future.race to coordinate multiple futures:

  • Future.all: Accepts an array of futures and returns a Future<Array<T>> that resolves when all input futures have succeeded, or fails immediately if any input future fails.
  • Future.race: Resolves or fails with the outcome of whichever input future settles first.
zena
let results = await Future.all([
  fetchEndpoint('/data/1'),
  fetchEndpoint('/data/2'),
  fetchEndpoint('/data/3'),
]);

let fastest = await Future.race([
  fetchFromPrimary(),
  fetchFromReplica(),
]);

Completers ​

A Completer<T> from zena:async serves as a manual bridge between callback-based APIs or external events and Zena futures:

zena
import { Completer } from 'zena:async';

let completer = new Completer<String>();

// Pass a callback to an external system:
hostEventEmitter.once('data', (data: String) => {
  completer.complete(data);
});

hostEventEmitter.once('error', (err: Error) => {
  completer.fail(err);
});

// Await the completer's future:
let result = await completer.future;

Timers and sleep ​

To pause execution asynchronously for a duration, use sleep from zena:time. It takes a Duration — a distinct type over i64 nanoseconds — built with helpers like milliseconds:

zena
import { sleep, milliseconds } from 'zena:time';

async function retryOperation(): Future<void> {
  for (var attempt = 1; attempt <= 3; attempt += 1) {
    try {
      await performTask();
      return;
    } catch (e) {
      console.log(`Attempt ${attempt} failed, retrying in 100ms...`);
      await sleep(milliseconds(100));
    }
  }
}

Structured cancellation ​

Long-running or redundant async tasks can be cancelled cooperatively using cancellation scopes.

Checkpoint delivery and unwind propagation ​

In async functions, cancellation is delivered at checkpoints (suspension points on await). When a task is cancelled, the cancellation propagates through the call stack like an exception, but on a dedicated runtime channel distinct from ordinary errors.

If a synchronous function is executing when cancellation is raised (for instance, by a cancellation check or an explicit cancellation call), the cancellation unwinds through all synchronous stack frames.

As cancellation unwinds through the stack:

  • finally blocks execute unconditionally in both synchronous and asynchronous functions, ensuring acquired files, locks, and network sockets are closed cleanly.
  • using resource declarations are automatically disposed.
  • catch (e) blocks ignore cancellation entirely and will not swallow it, preventing tasks from accidentally resuming cancelled work.
zena
import { TaskGroup } from 'zena:async';

let worker = async (): Future<void> => {
  try {
    while (true) {
      await performStep();
    }
  } finally {
    console.log('Worker cleaned up successfully');
  }
};

let group = new TaskGroup();
group.spawn(() => worker());

// Cancel the group to stop the worker at its next await:
group.cancel();

Only the group can cancel the work it started. The worker sees its scope read-only through currentScope(), so a task cannot cancel its siblings or the code that started it.

The cancel clause in try ​

In addition to catch and finally, a try block can define a cancel clause that runs specifically when the block is unwound due to cancellation:

zena
try {
  let rows = await db.query(queryString);
  return render(rows);
} cancel {
  metrics.increment('query_cancelled');
} catch (e) {
  return errorPage(e);
} finally {
  releaseBuffers();
}

The cancel block executes cancellation-specific rollback or logging, after which cancellation continues unwinding the stack unconditionally. Control-flow statements that suppress unwinding (such as return, break, or continue) are disallowed inside cancel blocks.

Cancellation in synchronous code ​

Because cancellation is delivered only at checkpoints, compute-intensive synchronous loops between await expressions do not yield control or observe cancellation automatically.

Synchronous routines that run for extended periods poll for cancellation cooperatively with checkCancellation() from zena:async, which unwinds the way an await checkpoint would if the ambient scope has been cancelled:

zena
for (let item in items) {
  checkCancellation();
  process(item);
}

Next ​

  • Control Flow — pattern matching, loops, and branching
  • Errors — exception handling and error propagation
  • Functions — top-level and arrow function declarations