Async Functions

    An async function is a function whose execution is split into an initiation, followed by an await completion. Its frame is provided explicitly by the caller, and it can be suspended and resumed any number of times.

    The code following the async callsite runs immediately after the async function first suspends. When the return value of the async function is needed, the calling code can await on the async function frame. This will suspend the calling code until the async function completes, at which point execution resumes just after the await callsite.

    Zig infers that a function is async when it observes that the function contains a suspension point. Async functions can be called the same as normal functions. A function call of an async function is a suspend point.

    At any point, a function may suspend itself. This causes control flow to return to the callsite (in the case of the first suspension), or resumer (in the case of subsequent suspensions).

    suspend_no_resume.zig

    Shell

    1. $ zig test suspend_no_resume.zig
    2. 1/1 test "suspend with no resume"... OK
    3. All 1 tests passed.

    In the same way that each allocation should have a corresponding free, Each suspend should have a corresponding resume. A suspend block allows a function to put a pointer to its own frame somewhere, for example into an event loop, even if that action will perform a resume operation on a different thread. provides access to the async function frame pointer.

    async_suspend_block.zig

    1. const std = @import("std");
    2. const expect = std.testing.expect;
    3. var the_frame: anyframe = undefined;
    4. var result = false;
    5. test "async function suspend with block" {
    6. _ = async testSuspendBlock();
    7. try expect(!result);
    8. resume the_frame;
    9. try expect(result);
    10. }
    11. fn testSuspendBlock() void {
    12. suspend {
    13. comptime try expect(@TypeOf(@frame()) == *@Frame(testSuspendBlock));
    14. the_frame = @frame();
    15. }
    16. result = true;
    17. }

    Shell

    1. $ zig test async_suspend_block.zig
    2. 1/1 test "async function suspend with block"... OK
    3. All 1 tests passed.

    suspend causes a function to be async.

    However, the async function can be directly resumed from the suspend block, in which case it never returns to its resumer and continues executing.

    resume_from_suspend.zig

    Shell

    1. $ zig test resume_from_suspend.zig
    2. 1/1 test "resume from suspend"... OK
    3. All 1 tests passed.

    This is guaranteed to tail call, and therefore will not cause a new stack frame.

    In the same way that every suspend has a matching resume, every async has a matching await in standard code.

    However, it is possible to have an async call without a matching await. Upon completion of the async function, execution would continue at the most recent async callsite or resume callsite, and the return value of the async function would be lost.

    async_await.zig

    1. const std = @import("std");
    2. const expect = std.testing.expect;
    3. test "async and await" {
    4. // The test block is not async and so cannot have a suspend
    5. // point in it. By using the nosuspend keyword, we promise that
    6. // the code in amain will finish executing without suspending
    7. // back to the test block.
    8. nosuspend amain();
    9. }
    10. fn amain() void {
    11. comptime try expect(@TypeOf(frame) == @Frame(func));
    12. const ptr: anyframe->void = &frame;
    13. resume any_ptr;
    14. await ptr;
    15. }
    16. fn func() void {
    17. suspend {}
    18. }

    Shell

    1. $ zig test async_await.zig
    2. 1/1 test "async and await"... OK
    3. All 1 tests passed.

    The await keyword is used to coordinate with an async function’s return statement.

    await is a suspend point, and takes as an operand anything that coerces to anyframe->T. Calling await on the frame of an async function will cause execution to continue at the await callsite once the target function completes.

    async_await_sequence.zig

    Shell

    1. $ zig test async_await_sequence.zig
    2. 1/1 test "async function await"... OK
    3. All 1 tests passed.

    In general, suspend is lower level than await. Most application code will use only async and await, but event loop implementations will make use of suspend internally.

    Putting all of this together, here is an example of typical async/await usage:

    async.zig

    1. const std = @import("std");
    2. const Allocator = std.mem.Allocator;
    3. pub fn main() void {
    4. _ = async amainWrap();
    5. // Typically we would use an event loop to manage resuming async functions,
    6. // but in this example we hard code what the event loop would do,
    7. // to make things deterministic.
    8. resume global_file_frame;
    9. resume global_download_frame;
    10. }
    11. fn amainWrap() void {
    12. amain() catch |e| {
    13. std.debug.print("{}\n", .{e});
    14. if (@errorReturnTrace()) |trace| {
    15. std.debug.dumpStackTrace(trace.*);
    16. }
    17. std.process.exit(1);
    18. };
    19. }
    20. fn amain() !void {
    21. const allocator = std.heap.page_allocator;
    22. var download_frame = async fetchUrl(allocator, "https://example.com/");
    23. var awaited_download_frame = false;
    24. errdefer if (!awaited_download_frame) {
    25. };
    26. var file_frame = async readFile(allocator, "something.txt");
    27. var awaited_file_frame = false;
    28. errdefer if (!awaited_file_frame) {
    29. if (await file_frame) |r| allocator.free(r) else |_| {}
    30. awaited_file_frame = true;
    31. const file_text = try await file_frame;
    32. defer allocator.free(file_text);
    33. awaited_download_frame = true;
    34. const download_text = try await download_frame;
    35. defer allocator.free(download_text);
    36. std.debug.print("download_text: {s}\n", .{download_text});
    37. std.debug.print("file_text: {s}\n", .{file_text});
    38. }
    39. var global_download_frame: anyframe = undefined;
    40. fn fetchUrl(allocator: Allocator, url: []const u8) ![]u8 {
    41. _ = url; // this is just an example, we don't actually do it!
    42. const result = try allocator.dupe(u8, "this is the downloaded url contents");
    43. errdefer allocator.free(result);
    44. suspend {
    45. global_download_frame = @frame();
    46. }
    47. std.debug.print("fetchUrl returning\n", .{});
    48. return result;
    49. }
    50. var global_file_frame: anyframe = undefined;
    51. fn readFile(allocator: Allocator, filename: []const u8) ![]u8 {
    52. _ = filename; // this is just an example, we don't actually do it!
    53. const result = try allocator.dupe(u8, "this is the file contents");
    54. errdefer allocator.free(result);
    55. suspend {
    56. global_file_frame = @frame();
    57. }
    58. std.debug.print("readFile returning\n", .{});
    59. return result;
    60. }

    Shell

    1. $ zig build-exe async.zig
    2. $ ./async
    3. readFile returning
    4. fetchUrl returning
    5. download_text: this is the downloaded url contents
    6. file_text: this is the file contents

    Now we remove the suspend and resume code, and observe the same behavior, with one tiny difference:

    blocking.zig

    Shell

    1. $ zig build-exe blocking.zig
    2. $ ./blocking
    3. fetchUrl returning
    4. readFile returning
    5. download_text: this is the downloaded url contents

    Previously, the fetchUrl and readFile functions suspended, and were resumed in an order determined by the function. Now, since there are no suspend points, the order of the printed “… returning” messages is determined by the order of async callsites.