Zig 0.16 delivers the release's most anticipated change: a clean-sheet async I/O system built around a new std.Io interface that you pass into functions the same way you already pass an allocator. Lead developer Andrew Kelley and community architect Loris Cro argue this sidesteps function coloring, the problem that forces languages like Rust and JavaScript to split code into async and non-async worlds. It is a bold, contested redesign, and it lands with one backend ready to use and another still cooking.

  • All I/O now flows through a caller-provided std.Io interface, mandatory for any input or output, modeled on how Zig already threads Allocator through code.
  • The goal is to decouple async I/O from the concurrency model and from the call site, so a function that does I/O does not get "colored" async by a keyword.
  • std.Io.Threaded (a thread pool with built-in cancellation) is feature-complete and shipping; std.Io.Evented (io_uring on Linux, kqueue on BSD/macOS) is a work in progress and does not fully compile yet.
  • Critics on Hacker News argue passing an Io parameter is itself a new form of coloring. Defenders note Io is a concrete value you can store, pass and swap at runtime.
Function coloring versus Zig's Io parameter In colored languages, async functions cannot be freely called from sync ones. In Zig 0.16, I/O is a value passed as a parameter, so functions stay uniform and the concurrency model is chosen by the caller. Colored (async/await) sync fn async fn (red) red cannot be called from blue without awaiting: viral split Zig 0.16 (Io parameter) fn read(io: Io, ...) fn parse(io: Io, ...) caller picks: Threaded / Evented same functions, no keyword color Io is passed like Allocator: the concurrency model lives in a value, not the syntax. genztech.blog
Fig 1 The core idea: instead of an async keyword that colors functions red or blue, Zig passes an Io value as a parameter, the way it already passes Allocator. The caller, not the syntax, chooses the concurrency model.

What changed in Zig 0.16?

Every I/O operation now requires an Io instance passed in by the caller. Anything that can block control flow or introduce nondeterminism, from reading a file to opening a socket, is owned by that interface. The fs APIs are fully migrated to Io, and net APIs are too, though the evented backend does not implement networking yet. This is the continuation of the standard-library I/O overhaul nicknamed "Writergate" in the 0.15 series, which made buffered I/O the default. The team is explicit that this migration is less painful than Writergate, because it is mostly mechanical.

RelatedGo 1.26's Green Tea GC Cuts Collection Overhead 40%

What is function coloring, and why does it matter?

In languages with an async keyword, functions come in two colors: async ("red") and normal ("blue"). Red functions can only be fully used from other red functions, so async spreads virally up your call stack and forces duplicate sync and async versions of libraries. Zig removed async/await several releases ago precisely because that model was too tightly coupled to stackless coroutines and could not serve everything from single-threaded microcontrollers to million-connection servers. The 0.16 redesign is the answer: keep functions uniform, and move the choice of execution model out of the syntax entirely.

How does passing Io as a parameter work?

Io behaves like Zig's Allocator interface: it is a concrete value, not a language keyword. A function that needs to do I/O accepts an Io parameter and uses it, without declaring itself async. The caller decides which implementation to hand in, which means the same function can run on a thread pool, an event loop or a test double, chosen at the call site or stored in application state. That flexibility is the whole argument. You are not annotating functions, you are dependency-injecting the concurrency model.

Threaded versus Evented: what actually ships?

Two backends exist, at very different maturity. std.Io.Threaded uses an OS thread pool, is feature-complete and well-tested, and bakes cancellation directly into the error sets: error.Canceled is part of every cancelable operation, and the recommended pattern is to defer a cancel right after spawning a task. std.Io.Evented, meant to use io_uring on Linux and kqueue on BSD and macOS, is still a work in progress that is missing functions and does not currently compile. If you want event-driven async today, third-party runtimes like zio already implement it with stackful coroutines over io_uring, epoll, kqueue or IOCP.

RelatedTypeScript 7 Go Compiler Hits RC and Runs 10x Faster

ModelZig std.IoRust asyncGo goroutinesNode.js
ColoringValue passed in (debated)async/await keywordsNone (runtime-managed)async/await keywords
Concurrency choiceCaller-selected backendRuntime crate (Tokio, etc.)Built-in schedulerSingle event loop
CancellationBaked into error setsDrop-based, trickycontext.ContextAbortController
StatusThreaded ready, Evented WIPMatureMatureMature

Is Io just coloring by another name?

That is the sharpest criticism, raised loudly on Hacker News: a function that takes Io cannot be called from one that does not, so the "viral" property returns through a different door. The defense is that Io is a concrete value with ordinary ergonomics: you can store it in a struct, pass it flexibly, swap implementations at runtime and test with a fake. That is materially different from a language-level keyword you cannot route around. Reasonable engineers disagree here, and 0.16 is where the argument stops being theoretical and starts being code people ship.

  1. earlierasync/await removed. Deemed too coupled to stackless coroutines.
  2. 0.15.x"Writergate". Buffered I/O by default, native x86 backend, faster debug builds.
  3. 0.16std.Io interface ships. Threaded backend ready; fs and net APIs migrated to Io.
  4. nextstd.Io.Evented matures. io_uring / kqueue backend, plus networking support.
What to watch · Zig roadmap
  • Evented backend lands. Until io_uring support compiles and ships, high-throughput servers lean on third-party runtimes like zio.
  • Does the coloring debate settle? Real codebases on std.Io will show whether the Io parameter is ergonomic or just relocated pain.
  • Migration cost. The team promises this is easier than Writergate. Watch how the ecosystem's libraries adapt.

Our take

This is the most interesting concurrency redesign in a mainstream systems language right now, precisely because it refuses the async keyword everyone else copied. Passing Io like Allocator is elegant and very Zig: explicit, injectable, testable. The honest caveat is that 0.16 ships the idea half-built, with only the threaded backend ready and the io_uring one still broken, so the performance story that justifies the whole thing is deferred. And the coloring critics are not wrong that a mandatory parameter has viral properties of its own. But Zig has earned the benefit of the doubt by shipping clean-sheet designs before, and if the evented backend delivers, this becomes the reference for how to do async without splitting a language in two.

Primary sources

Original analysis by GenZTech. Details from the Zig 0.16.0 release notes, 2026.