Async Await in Zig

For years, asynchronous programming in systems languages has been painful. You either suffered from “function coloring” (where every function had to be marked async), or you dealt with complex callbacks, or you gave up and used threads everywhere.

With the brand new std.Io abstraction and a fresh take on async and await, Zig now offers one of the most elegant solutions to asynchronous I/O in any programming language. You write normal, synchronous-looking code that works with both threaded and high-performance evented backends — without polluting your entire codebase with async keywords.

const std = @import("std");
const Io = std.Io;

pub fn main(init: std.process.Init) !void {
    const gpa = init.gpa;   // Ready-to-use general purpose allocator
    const io = init.io;     // Default I/O backend

    try juicyMain(gpa, io);
}

// Application logic
fn juicyMain(gpa: std.mem.Allocator, io: Io) !void {
    _ = gpa; // suppress unused parameter warning

    std.debug.print("Starting async tasks...\n", .{});

    // Launch tasks asynchronously
    var task1 = io.async(calcSum, .{ 1, 500_000 });
    defer _ = task1.cancel(io);   // cancel returns the result type (usize)

    var task2 = io.async(calcSum, .{ 500_001, 1_000_000 });
    defer _ = task2.cancel(io);

    // You can perform other work here while the tasks run...

    // Retrieve the results
    const sum1 = task1.await(io);
    const sum2 = task2.await(io);

    std.debug.print("Total sum = {}\n", .{sum1 + sum2});
}

// Normal synchronous function — no async keywords needed
fn calcSum(start: usize, end: usize) usize {
    var sum: usize = 0;
    for (start..end + 1) |i| {
        sum += i;
    }
    std.debug.print("calcSum({}..{}) finished\n", .{ start, end });
    return sum;
}

We start with pub fn main(init: std.process.Init) !void, which is the recommended entry point in Zig 0.16. This gives us two powerful objects for free: init.gpa (a general-purpose memory allocator) and init.io (the I/O context that powers asynchronous operations). We pass these directly to our application logic function juicyMain.

Inside juicyMain, we launch two tasks using io.async(calcSum, .{arguments}). This function takes any normal Zig function and its arguments, schedules it to run asynchronously, and immediately returns a Future (a handle to the running task). Notice that calcSum is a completely ordinary function — it has no async keyword and knows nothing about concurrency.

We use defer _ = task1.cancel(io); right after creating each task. This is crucial: cancel() ensures that the task’s resources are properly cleaned up whether the task has already finished or is still running. In this case, since calcSum returns a usize, cancel() also returns a usize (the result if the task completed), which we ignore with _ =.

Later, we call task1.await(io) and task2.await(io). These calls block until their respective tasks finish and then return the computed values. Because the two tasks can run concurrently (depending on the I/O backend), the total execution time is significantly reduced compared to running them sequentially.

Finally, the calcSum function itself is straightforward: it calculates the sum of numbers in a range and prints a completion message. This separation — normal functions + io.async/await — is the heart of Zig’s new design.

Save the Zig code as await.zig and execute it:

$ zig run await.zig
Starting async tasks...
calcSum(1..500000) finished
calcSum(500001..1000000) finished
Total sum = 500000500000

The program first prints “Starting async tasks…” to indicate that the main() function has begun. It then launches both calcSum tasks concurrently using io.async(). Because these tasks can run in parallel (thanks to the I/O backend), their completion messages appear shortly after — often nearly at the same time.

You will notice that the two calcSum functions finish independently and print their messages as soon as they complete. Finally, after both tasks have been awaited, the program prints the combined result: 500000500000, which is the correct sum of all integers from 1 to 1,000,000.

The key takeaway is that even though the code reads like simple sequential programming, the two heavy calculations actually executed concurrently. This gives you the performance benefits of concurrency while keeping your code clean, readable, and free from traditional async/await function coloring.

Happy coding in Zig!