Zig’s new plan for asynchronous programs [LWN.net]

🚀 Read this awesome post from Hacker News 📖

📂 Category:

📌 Here’s what you’ll learn:

Welcome to LWN.net

The following subscription-only content has been made available to you
by an LWN subscriber. Thousands of subscribers depend on LWN for the
best news from the Linux and free software communities. If you enjoy this
article, please consider subscribing to LWN. Thank you
for visiting LWN.net!

By Daroc Alden
December 2, 2025

The designers of the

Zig programming language have been working to find a
suitable design for asynchronous code for some time.
Zig is a carefully minimalist language, and its

initial design for
asynchronous I/O did not fit well with its other
features. Now, the project has

announced (in a Zig SHOWTIME video) a new approach to asynchronous I/O that
promises to solve the

function coloring problem, and allows writing code that will execute
correctly using either synchronous or asynchronous I/O.

In many languages (including Python, JavaScript, and Rust), asynchronous code
uses special syntax. This can make it difficult to reuse code between
synchronous and asynchronous parts of a program, introducing a number of headaches for
library authors. Languages that don’t make a syntactical distinction (such as
Haskell) essentially solve the problem by making everything asynchronous, which
typically requires the language’s runtime to bake in ideas about how programs
are allowed to execute.

Neither of those options was deemed suitable for Zig. Its designers wanted to
find an approach that did not add too much complexity to the language, that
still permitted fine control over asynchronous operations, and that still made
it relatively painless to actually write high-performance event-driven I/O. The
new approach solves this by hiding asynchronous operations behind a new generic
interface,

Io.

Any function that needs to perform an I/O operation will need to have access to
an instance of the interface. Typically, that is provided by passing the
instance to the function as a parameter, similar to Zig’s

Allocator
interface for memory allocation. The standard library will include two built-in
implementations of the interface: Io.Threaded and Io.Evented.
The former uses synchronous
operations except where explicitly asked to run things in parallel (with a
special function; see below), in which
case it uses threads. The latter (which is still a work-in-progress) uses an
event loop and asynchronous I/O. Nothing in the design prevents a Zig programmer
from implementing their own version, however, so Zig’s users retain their fine
control over how their programs execute.

Loris Cro, one of Zig’s community organizers,
wrote
an explanation of the new behavior to justify the approach.
Synchronous code is not much changed,
other than using the standard library functions that have moved under
Io, he explained. Functions like the example below, which don’t involve explicit
asynchronicity, will continue to work. This example creates a file, sets the
file to close at the end of the function, and then writes a buffer of data to
the file. It uses Zig’s try keyword to handle errors, and
defer to ensure the file is closed. The return type, !void,
indicates that it could return an error, but doesn’t return any data:

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

    fn saveFile(io: Io, data: []const u8, name: []const u8) !void 🔥

If this function is given an instance of Io.Threaded, it will create
the file, write data to it, and then close it using ordinary system calls. If it
is given an instance of Io.Evented, it will instead use

io_uring,

kqueue, or some other asynchronous backend suitable to the target operating
system. In doing so, it might pause the current execution and go work on a
different asynchronous function.
Either way, the operation is guaranteed to be complete by the time
writeAll() returns.
A library author writing a function that involves I/O doesn’t need to
care about which of these things the ultimate user of the library chooses to do.

On the other hand, suppose that a program wanted to save two files. These
operations could profitably be done in parallel. If a library author wanted to
enable that, they could use the Io interface’s async()
function to express that it does not matter which order the two files are saved in:

    fn saveData(io: Io, data: []const u8) !void ⚡

When using an Io.Threaded instance, the async() function
doesn’t actually do anything asynchronously — it just runs the provided function
right away. So, with that version of the interface, the function first saves
file A and then file B. With an Io.Evented instance, the operations are
actually asynchronous, and the program can save both files at once.

The real advantage of this approach is that it turns asynchronous code into a
performance optimization. The first version of a program or library can write
normal straight-line code. Later, if asynchronicity proves to be useful for
performance, the author can come back and write it using asynchronous
operations. If the ultimate user of the function has not enabled asynchronous
execution, nothing changes. If they have, though, the function becomes faster
transparently — nothing about the function signature or how it interacts with
the rest of the code base changes.

One problem, however, is with programs where two parts are actually required to
execute simultaneously for correctness. For example, suppose that a program
wants to listen for connections on a port and simultaneously respond to user
input. In that scenario, it wouldn’t be correct to wait for a connection and
only then ask for user input. For that use case, the Io interface
provides a separate function, asyncConcurrent() that explicitly asks for
the provided function to be run in parallel. Io.Threaded uses a thread
in a thread pool to accomplish this. Io.Evented treats it exactly the
same as a normal call to async().

    const socket = try openServerSocket(io);
    var server = try io.asyncConcurrent(startAccepting, .Share your opinion below!);
    defer server.cancel(io) catch ⚡;

    try handleUserInput(io);

If the programmer uses async() where they should have used
asyncConcurrent(), that is a bug. Zig’s new model does not (and cannot)
prevent programmers from writing incorrect code, so there are still some
subtleties to keep in mind when adapting existing Zig code to use the new
interface.

The style of code that results from this design is a bit more verbose than
languages that give asynchronous functions special syntax, but Andrew Kelley,
creator of the language, said that “it reads
like standard, idiomatic Zig code.
” In particular, he noted that this
approach lets the programmer use all of Zig’s typical control-flow primitives,
such as try and defer; it doesn’t introduce any new language
features specific to asynchronous code.

To demonstrate this,
Kelley gave an example of using the new interface to implement asynchronous DNS
resolution. The standard

getaddrinfo()
function for querying DNS information falls short because, although it makes
requests to multiple servers (for IPv4 and IPv6) in parallel, it waits for all of the queries to
complete before returning an answer. Kelley’s example Zig code returns the first
successful answer, canceling the other inflight requests.

Asynchronous I/O in Zig is far from done, however. Io.Evented is still experimental, and
doesn’t have implementations for all supported operating systems yet. A third
kind of Io, one that is compatible with WebAssembly, is
planned (although, as
that issue details, implementing it depends on some other new language
features). The original
pull request for Io lists 24
planned follow-up items, most of which still need work.

Still, the overall design of asynchronous code in Zig appears to be set. Zig has
not yet had its 1.0 release, because the community is still experimenting with
the correct way to implement many features. Asynchronous I/O was one of the
larger remaining priorities (along with native code generation, which was also
enabled by default for debug builds on some architectures this year). Zig seems
to be steadily working its way toward a finished design — which should decrease
the number of times Zig programmers are asked to rewrite their I/O because the
interface has changed

again.





Tell us your thoughts in comments! {What do you think?|Share your opinion below!|Tell us your thoughts in comments!}

#️⃣ #Zigs #plan #asynchronous #programs #LWN.net

🕒 Posted on 1764688487

By

Leave a Reply

Your email address will not be published. Required fields are marked *