await means: wait until an asynchronous operation has finished, then continue with its result.
await means: wait until an asynchronous operation has finished, then continue with its result.
A suspension point is a place where the current operation may pause so something else can make progress.
In simple words:
async starts work
await waits for work
suspension lets other work runThis is the basic shape of asynchronous programming.
Start First, Wait Later
A normal function call works like this:
const value = doWork();
use(value);The program cannot continue until doWork() finishes.
Async code separates starting from waiting:
const future = io.async(doWork, .{});
const value = try future.await(io);
use(value);The first line starts the work.
const future = io.async(doWork, .{});The second line waits for the result.
const value = try future.await(io);The value returned by io.async is a future. A future is a handle to work that may still be running.
What await Does
When you call await, you are saying:
I need this result before I can continue.If the work is already finished, await returns quickly.
If the work is still running, await waits.
If the work failed, await returns the error.
That is why async code often uses try:
const value = try future.await(io);This means:
wait for the future
if it failed, return the error
otherwise, give me the valueAwait Is a Boundary
Before await, the operation may be unfinished.
After await, the operation is complete.
const future = io.async(readFile, .{ path });
// The read may still be in progress here.
const contents = try future.await(io);
// The read is finished here.This boundary is important for ownership.
Before the future completes, you must keep alive anything the operation needs. That includes buffers, paths, pointers, allocators, and shared state.
Suspension
A suspension point is where a task may stop temporarily.
In async code, waiting usually creates a suspension point.
const result = try future.await(io);If the future is not ready, the current task may suspend. The program can then run other work while this task is waiting.
This is the main benefit of async code. Waiting does not have to waste the whole program’s time.
Suspension Is Not Random
A beginner may imagine async code can stop anywhere. That would make programs impossible to reason about.
A better mental model is:
Async code may suspend only at known waiting points.
In code using futures, those waiting points are visible:
try future.await(io);That visible await is useful. It tells you where the function may pause.
Why Visible Suspension Matters
Suppose you have code like this:
state.beginUpdate();
const data = try future.await(io);
state.finishUpdate(data);The await happens between beginUpdate and finishUpdate.
That may be a problem. While this task is waiting, other work may run. That other work might observe state halfway through an update.
A safer design is often:
const data = try future.await(io);
state.beginUpdate();
state.finishUpdate(data);Wait first. Then update state.
The rule is:
Avoid holding half-finished state across an await.
Do Not Hold Locks Across Await
This is especially important with mutexes.
Bad pattern:
state.mutex.lock();
defer state.mutex.unlock();
const data = try future.await(io);
state.value = data;The lock is held while waiting.
That can block other code for a long time. In worse cases, it can deadlock if the awaited operation needs something that also requires the same lock.
Better:
const data = try future.await(io);
state.mutex.lock();
defer state.mutex.unlock();
state.value = data;Wait first. Lock only when updating the shared state.
The same principle appeared with threads: keep critical sections small. With async code, this becomes even more important.
Awaiting Several Futures
If you start two independent operations, start both before awaiting either one.
Less useful:
const a = try io.async(readA, .{}).await(io);
const b = try io.async(readB, .{}).await(io);This starts readA, waits for it, then starts readB.
Better shape:
const future_a = io.async(readA, .{});
defer future_a.cancel(io) catch {};
const future_b = io.async(readB, .{});
defer future_b.cancel(io) catch {};
const a = try future_a.await(io);
const b = try future_b.await(io);Now both operations are started before waiting for their results.
This gives the I/O backend a chance to overlap them.
Await Order Still Matters
Even if you start several futures first, the order of await can affect behavior.
const a = try future_a.await(io);
const b = try future_b.await(io);This waits for a first.
If b finishes early, the code still waits for a before handling b.
Sometimes that is fine. Sometimes you want whichever result finishes first. That requires a different structure, usually an event loop, queue, or higher-level I/O pattern.
For beginners, the simple rule is enough:
Start independent work first. Await when you need the result.
Cancellation and Await
A future should not be abandoned.
If you start work and then leave the scope, the program must clean up that work.
That is why async examples often use this pattern:
const future = io.async(doWork, .{});
defer future.cancel(io) catch {};
const result = try future.await(io);The defer protects early exits.
If await succeeds normally, the future is finished. If an error or early return happens before that, cancel gives the operation a chance to stop and release resources.
This is the same Zig habit used elsewhere:
resource.acquire();
defer resource.release();Async work is a resource too.
Await and Errors
A future returns whatever the async operation returns.
If the operation returns an error union, await returns that error union too.
For example, suppose the operation has this shape:
fn readConfig() !Config {
// may fail
}Then awaiting its future may fail:
const future = io.async(readConfig, .{});
defer future.cancel(io) catch {};
const config = try future.await(io);The try belongs at the await point because that is where the final result is collected.
Await and Lifetimes
This is one of the most important rules.
Any data used by the async operation must stay alive until the operation is finished or cancelled.
Bad:
fn startRead(io: std.Io) !void {
var buffer: [1024]u8 = undefined;
const future = io.async(readInto, .{&buffer});
_ = future;
}The function returns while the operation may still use buffer.
Good:
fn readNow(io: std.Io) !void {
var buffer: [1024]u8 = undefined;
const future = io.async(readInto, .{&buffer});
defer future.cancel(io) catch {};
_ = try future.await(io);
}Here, buffer stays alive until after await.
Suspension and Local Variables
When a task suspends, its local variables may need to remain available until it resumes.
That is one reason async runtimes and compilers need careful machinery. A normal function call can often store locals on the stack and remove them when the function returns. An async task may pause and resume later.
As a beginner, you do not need to know all implementation details. You do need the practical rule:
Do not assume async work ends just because the current line has finished.
It ends when you await it, cancel it, or otherwise know it has completed.
Async Code Should Have Clear Ownership
When writing async code, ask:
Who owns the future?
Who awaits it?
Who cancels it if something goes wrong?
What memory does the operation use?
How long must that memory live?
These questions prevent most beginner async bugs.
A clean function starts async work and finishes responsibility for it in the same scope:
const future = io.async(doWork, .{});
defer future.cancel(io) catch {};
const result = try future.await(io);A more advanced program may pass futures around, but then ownership must be documented clearly.
The Main Rule
await is the point where async work becomes a real result.
Before await, you have a promise of future completion.
After await, you have the value or the error.
Suspension means the current task may pause while waiting. That pause is useful, but it also affects locks, lifetimes, and shared state.
Write async code so the waiting points are easy to see. Do not hold locks across waits. Do not keep half-updated state across waits. Keep every buffer and pointer alive until the future is finished or cancelled.