Pruning large inferred error sets

I’ve got a large inferred error set that I am trying to prune down:

scanner.readEni(allocator, args.preop_timeout_us, false, args.pv_name_prefix) catch |err| switch (err) {
      error.LinkError,
      error.Overflow,
      error.NoSpaceLeft,
      error.OutOfMemory,
      error.RecvTimeout,
      error.Wkc,
      error.StateChangeRefused,
      error.StateChangeTimeout,
      error.Timeout,
      error.UnexpectedSubdevice,
      error.InvalidSII,
      error.InvalidMbxConfiguration,
      error.CoENotSupported,
      error.CoECompleteAccessNotSupported,
      error.Emergency,
      error.NotImplemented,
      error.MbxOutFull,
      error.InvalidMbxContent,
      error.MbxTimeout,
      error.Aborted,
      error.WrongProtocol,
      error.MissedFragment,
      error.InvalidMailboxContent,
      error.ObjectDoesNotExist,
      error.InvalidCoE,
      error.EndOfStream,
      => continue :bus_scan,
};

So I start by deleting error.EndOfStream from the switch case so I can use compile errors to track down all the usages. However, the compile errors don’t really give me an error trace:

src/cli/run.zig:158:114: error: switch must handle all possibilities
            break :blk scanner.readEni(allocator, args.preop_timeout_us, false, args.pv_name_prefix) catch |err| switch (err) {
                                                                                                                 ^~~~~~
src/cli/run.zig:158:114: note: unhandled error value: 'error.EndOfStream'
referenced by:
    main: src/cli/main.zig:62:39
    main: /home/jeff/.config/Code/User/globalStorage/ziglang.vscode-zig/zig/x86_64-linux-0.14.1/lib/std/start.zig:660:37
    comptime: /home/jeff/.config/Code/User/globalStorage/ziglang.vscode-zig/zig/x86_64-linux-0.14.1/lib/std/start.zig:58:30
    start: /home/jeff/.config/Code/User/globalStorage/ziglang.vscode-zig/zig/x86_64-linux-0.14.1/lib/std/std.zig:97:27
    comptime: /home/jeff/.config/Code/User/globalStorage/ziglang.vscode-zig/zig/x86_64-linux-0.14.1/lib/std/std.zig:168:9
error: the following command failed with 1 compilation errors:

It would be nice if the compiler could tell me where errors come from.

You wouldn’t get an error trace, that’s a compile error and error traces are at runtime.

It might be nice to have an “error can arise here” line in the compile error, if that’s what you mean. There can be arbitrarily many such places but maybe that’s not a problem.

I think most users who see an error like that are going to want to add the switch, not find the place where that error can arise. But that’s just a guess.

What does it signify to “prune” this error set? Are you looking to ignore some of the possible errors? Turn them into panics? Remove the code which can return them?

2 Likes

i’ve just got a lot of inferred error set technical debt that I need to pay off. Lots of these errors shouldn’t make it up this far and should be handled / renamed earlier. This function is exposed to my (non-existent) library users, and when a user sees “EndOfStream” here it won’t be useful to them compared to some of the other errors.

1 Like

Ah that make sense.

Probably want to just filter out the ones where if they’re triggered it’s a bug in your code or a dependency.

I did that in the last minor point release of zg. Allocating the tries for various data sets can in principle produce this or that decompression error, it was a rather long list, but a) user code can’t do anything about it, and b) that’s a bug I need to handle if it arises. So I turned everything except error.OutOfMemory into an unreachable.

So for that case, just handle all the switches it tells you about and turn them into an unreachable branch before the user has to see them.

Its a little bit easier to track down if you add an error set to the function definition and add one error at a time:

pub fn readEni(
    self: *Scanner,
    allocator: std.mem.Allocator,
    state_change_timeout_us: u32,
    /// Read information for simulator.
    /// Not required unless you are running the simulator.
    sim: bool,
    pv_name_prefix: ?[]const u8,
) error{}!gcat.Arena(ENI) {

Then you start getting compile errors that show you where errors come from:

src/module/Scanner.zig:245:19: error: expected type 'error{}', found 'error{OutOfMemory}'
    const arena = try allocator.create(std.heap.ArenaAllocator);
                  ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/module/Scanner.zig:245:19: note: 'error.OutOfMemory' not a member of destination error set

Only goes as deep as the current function through, so you still have to search slowly.

3 Likes

I was going to suggest adding an explicit error set to readEni that does not include EndOfStream to potentially get some better diagnostics, but that gives an equally unhelpful error:

Test program
fn foo() !void {
    var a: u32 = 1;
    _ = &a;
    switch (a) {
        0 => return error.Foo,
        1 => return error.Bar,
        2 => return error.Baz,
        else => return,
    }
}

const BarError = error{ Foo, Bar };

fn bar() BarError!void {
    try foo();
}

pub fn main() !void {
    bar() catch |err| switch (err) {
        error.Foo,
        error.Bar,
        => {},
    };
}
errsets.zig:15:5: error: expected type 'error{Foo,Bar}', found '@typeInfo(@typeInfo(@TypeOf(errsets.foo)).@"fn".return_type.?).error_union.error_set'
    try foo();
    ^~~~~~~~~
errsets.zig:15:5: note: 'error.Baz' not a member of destination error set

which gets us back to

However, if you add an explicit error set (that does not include the error you want to exclude) to the actual function that can return the error, the compiler will tell you where it can occur:

Test program
const FooError = error{ Foo, Bar };

fn foo() FooError!void {
    var a: u32 = 1;
    _ = &a;
    switch (a) {
        0 => return error.Foo,
        1 => return error.Bar,
        2 => return error.Baz,
        else => return,
    }
}

fn bar() !void {
    try foo();
}

pub fn main() !void {
    bar() catch |err| switch (err) {
        error.Foo,
        error.Bar,
        => {},
    };
}
errsets.zig:9:27: error: expected type 'error{Foo,Bar}', found type 'error{Baz}'
        2 => return error.Baz,
                          ^~~

May still not be helpful in your situation, but thought I’d mention it.

In my opinion, using explicit error sets is probably the right tool for the job, but the compiler currently doesn’t provide much that makes working with explicit error sets nice, unfortunately.