Ok, I’ll admit I’ve been dragging my feet a bit to understand the ‘new’ Io. Today I’ve been diving into it, and in particular I’m trying to understand when to handle error.Canceled, and when it could happen.

From the 0.16.0 release notes Andrew writes:

Only the logic that made the cancelation request can soundly ignore an error.Canceled. Otherwise, there are three ways to handle error.Canceled. In order of most common:

  1. Propagate it.
  2. After receiving it, io.recancel() and then don’t propagate it. This rearms the cancelation request, so that the next check will have a chance to detect and acknowledge the request.
  3. Make it unreachable with io.swapCancelProtection().

And then there is an example:

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

test "trivial cancel demo" {
    const io = std.testing.io;

    var file_task = io.async(Io.Dir.openFile, .{ .cwd(), io, "hello.txt", .{} });
    defer if (file_task.cancel(io)) |file| file.close(io) else |_| {};
}

Assumption 1: When it says ‘only logic that made the cancelation request can soundly ignore an error.Canceled’ I read that as you actually have to be handling the result of a call to .cancel(). Is that right?

Continuing on: essentially all Io functions now return an Io.Cancelable error (error.Canceled). This allows them to be used as cancellation points. From the docs of CancelProtection:

In rare cases, it is desirable to completely block cancelation notification, so that a region of code can run uninterrupted before error.Canceled is potentially observed. Therefore, every task has a “cancel protection” state which indicates whether or not Io functions can introduce cancelation points.

To modify a task’s cancel protection state, see swapCancelProtection.

Assumption 2: the point of having Io functions return error.Canceled is so that they can be used as cancelation points, but that if I’m positive that no cancelation is going to happen, i’m free to ignore or even unreachable the error.

For example consider this simple function that reads in a path from the command line and opens that directory.

pub fn main(init: std.process.Init) !void {
    const io = init.io;
    const arena = init.arena.allocator();
    const args = init.minimal.args;

    const prog = args.vector[0];
    if (args.vector.len <= 1) usage(io, prog);
    const path = args.vector[2];

    const dir = Io.Dir.openDir(io, path) catch |err| {
        switch(err) {
            error.Canceled => unreachable,
            else => return err,
        }
    };
    _ = dir; // Do something with it later
}

Since the function to open is not called in an async/concurrent context, am I free to mark error.Canceled as unreachable? Or am I missing something. Obviously in this simple example cancelation is impossible, because canceled is never called. What about more complex scenarios. If an Io function is called in a blocking way, can it receive an error.Canceled?