Preferred way to handle iterators that can fail?

I’ve had to use/write several iterators where the next() method has a signature similar to this:

pub fn next(*it: Iter) error{Doh}!?Item

I’m curious to see how the community prefers to handle these cases. I typically use one of three depending on the context:

// 1. try-ing the error
// I like this when I don't care about handling the iterator error, but
// I find that often when I'm using an iterator I don't want the caller
// to fail when the iterator fails - I'd rather treat it as the "natural"
// end of the iteration.
while (try it.next()) |item| {...}

// 2. Unwrapping the optional
// Usually my go-to. I typically use this when an error always means that the 
// iteration should end, but the caller can continue.
while (it.next()) |maybe_item| {
    if (maybe_item) |item| { ... } else break;
} else |err| { ... }

// 3. while true
// I use this the least because it's a bit awkward. This example is very
// simplified - usually in the catch block I will handle errors in different
// ways. Generally, at least one error will still allow the iteration to continue.
while (true) {
    const item = it.next() catch { break; } orelse break;
    ...
}

Do you have a preferred way of handling these iterations? Did I miss any obvious alternatives?

while (canError()) |res| if (res) |payload| {
    // happy path
} else break else |err| {
   // Error handler
}
6 Likes

#2, but with result and value. I like @mnemnion‘s compaction, though maybe less legible at first sight(?)

The third case might be less awkward if you modified the iterator signature like so:

fn next(self: *Iterator) ?error{ Halt, Pass }!i32

Then, the iteration could look like this:

while (it.next()) |val_err| {
    const val = val_err catch |err| switch (err) {
        error.Pass => {
            // TODO: handle error
            continue;
        },
        error.Halt => {
            // TODO: handle error
            break;
        },
    };
    // TODO: do something with `val`
}

if can capture errors too, if you prefer the happy path at the top:

while (it.next()) |maybe_error| {
    if (maybe_error) |result| {
        log.debug("{d}", .{result});
    } else |err| switch (err) {
        error.Pass => {
            log.debug("", .{});
            continue;
        },
        error.Halt => {
            log.debug("break!", .{});
            break;
        },
    }
}
1 Like

If you dont want to try, I’d do this just to avoid additional indentation.

while (it.next()) |maybe_item| {
    const item = maybe_item orelse break;
    // ...
} else |err| { ... }
1 Like

You didn’t try to compile this, did you?

Yes I did? :sweat_smile:

scratch.zig
// 0.15.2 x86_64-windows-msvc
pub fn main() !void {
    var prng = std.Random.DefaultPrng.init(
        @abs(std.time.microTimestamp()) % std.time.us_per_s,
    );
    const random = prng.random();

    var it: Iterator = .{
        .count = 10,
        .prng = random,
    };
    while (it.next()) |maybe_error| {
        if (maybe_error) |result| {
            log.debug("{d}", .{result});
        } else |err| switch (err) {
            error.Pass => {
                log.debug("", .{});
                continue;
            },
            error.Halt => {
                log.debug("break!", .{});
                break;
            },
        }
    }
}
pub const Iterator = struct {
    count: i32,
    prng: std.Random,

    pub fn next(self: *Iterator) ?error{ Halt, Pass }!i32 {
        const value = self.count;
        if (value == 0) return null;
        if (self.prng.float(f32) > 0.9) return error.Halt;
        if (self.prng.float(f32) > 0.6) return error.Pass;
        self.count -= 1;
        return value;
    }
};

pub const std = @import("std");
pub const log = std.log.scoped(.scratch);

C:\local\zig\0.15.2\zig.exe run -target x86_64-windows-msvc .\scratch.zig
debug(scratch):
debug(scratch):
debug(scratch): 10
debug(scratch): 9
debug(scratch):
debug(scratch): break!
2 Likes

First time I’ve seen a ?!T rather than a !?T but, fair play.

2 Likes

For extra credit, turns out oops, you really want the caller to handle the errors on this use of the iterator.

What does that refactor look like?