Did errdefer comptime unreachable just become more powerful?

Upfront warning: this code relies on capturing errdefer, and there’s an accepted proposal to remove it from the language

Justus’ sweet rewrite of the switch logic got merged and I think that’s an enabler for the following idiom:

const std = @import("std");

pub fn fails(fail: bool) !void {
    return if (fail) error.SomeError else {};
}

pub fn main() !void {
    var debug_allocator = std.heap.DebugAllocator(.{}){};
    const gpa = debug_allocator.allocator();
    const data = try gpa.alloc(u8, 1024);
    defer gpa.free(data);

    // Here we express that we disallow OOM errors going forward, but
    // we do allow other errors to propagate. You can obviously
    // add more prongs as needed.
    // However, this feature may be removed: https://github.com/ziglang/zig/issues/23734
    errdefer |err| {
        switch (err) {
            error.OutOfMemory => comptime unreachable,
            inline else => {},
        }
    }

    try fails(true);
}

This is a variation of errdefer comptime unreachable, but we get to cherry-pick which errors we want to block and allow. In this example, OOM can only happen above errdefer, but we allow other errors.

This won’t compile on 0.15.x due to 'error.OutOfMemory' not a member of destination error set, but it does compile on latest master after Justus’ improvements.

Anyway, I haven’t tested this in real-world code and capturing errdefer is slated for removal, but figured I’d mention it.

10 Likes

Just how is this better than

const std = @import("std");

pub fn fails(fail: bool) error{SomeError}!void {
    return if (fail) error.SomeError else {};
}

pub fn main() error{SomeError, OutOfMemory}!void {
    var debug_allocator = std.heap.DebugAllocator(.{}){};
    const gpa = debug_allocator.allocator();
    const data = try gpa.alloc(u8, 1024);
    defer gpa.free(data);

    try fails(true);
    _ = gpa.alloc(u8, 1) catch |err| {
        if (err == error.OutOfMemory) unreachable;
        return;
    };
}

and any explicit variant thereof?

This has very different semantics, tho.

1 Like

See this and this for examples of the use case of statically preventing further errors rather than at runtime.

1 Like

ok, I see, the OP’s variant is kind of like removal of that error variant from the return site from that point forward. which is real smart, sadly. just that little bit of almost magic that zig lacks everywhere else.

it can also just be reordered to be explicit and stupid. for example, by simply lacking calls that could return that error variant, and making returned error sets explicit so that you could not return it?

something like

pub fn main() error{ SomeError, OutOfMemory }!void {
    var debug_allocator = std.heap.DebugAllocator(.{}){};
    const gpa = debug_allocator.allocator();
    const data = try gpa.alloc(u8, 1024);
    defer gpa.free(data);
    try mainInnerNoAlloc();
}

fn mainInnerNoAlloc() error{SomeError}!void {
    // no error.OutOfMemory here, obviously
    try fails(true);
}

anyways, to each his own!

Well, the original snippet is just enough to express the idiom and not an example where it’s actually useful. What if you scale up?

A larger function can have SomeError in the error set (heck, maybe you’re forced to use anyerror), and it may be useful to express it’s never ever returned “from this point” through that static assertion. That’s refactor-proofing and useful information to the reader imo.

I don’t think this would ever be common, just wondering if it’s a useful tool to have.

2 Likes

I like it, it’s clever in a good way. I don’t view it as itself a good case for keeping errdefer |err|.

“capturing errdefer” can’t handle errors, it can only look at them. In a sense what you’re doing here is cheating that contract, only in a sense, comptime is not runtime, but I would just as soon make the other case: what you’re demonstrating is a use of capturing errdefer to handle errors, not just glance at them. Handling them at a comptime level is merely a special case which happens to be permitted in status quo.

An errdefer |err| which truly captures the error, such that the block has the same result type as the function and must return something of that type, would be powerful enough to justify its own existence. I did suggest exactly this, at one point, but that went nowhere.

It would make Zig more complex, and your example of doing so at comptime only is a good illustration of that fact as well. It creates a bit of logic which follows the block down the function scope, affecting everything it passes. One can argue the same to be true of errdefer comptime unreachable; itself, correctly so, but this is binary, and the effect is very simple to reason about: no more errors may be returned, period.

On the other hand, going the distance, having errdefer |err| actually capture the error, would enable some fairly eloquent programming. I would go so far as to say that capturing the error without any ability to handle the capture error is a crippled idiom: handling some, perhaps all, of the possible errors, is the most attractive reason to capture its value.

Overall, in order, my personal preferences are: remove it, fix the broken semantics, and in last place, let it keep limping along being what it is.

1 Like

This exact topic (the power of comptime unreachable) came up on the zulip.

Honestly, if anything I think that’s an argument for getting rid of capturing errdefer. IMO errors should not be used for default flow control. Which is just a different way of saying errors should be avoided. The more features you pack into errdefer, the more you’re encouraging people to “misuse” it. I don’t mean to imply that handling errors correctly, or safely, or trying to be fancy should be considered incorrectly. But being fancy with errors, when you have the opportunity to write code that can’t cause an error is incorrect[1]. Consider the code that uses capturing errdefer comptime unreachable, vs a bare errdefer comptime unreachable. I’d argue the latter code is much more likely to be significantly better.

I also, feel that if you find yourself in need of a capturing errdefer… your function is probably too complex, or trying to do too much.


  1. for some definitions of incorrect ↩︎

2 Likes

I took my time getting there, but I agree.