Is there a deep reason why the type of (error{A, B}).A is error{A, B}, rather than just error{A}?
I don’t think there’s a deep reason behind it; the design intent of ErrorSet.Foo is probably just to represent @as(ErrorSet, error.Foo), to differentiate it from the type of error.Foo.
That’s why there’s a proposal to remove ErrorSet.Foo.
I had a coworker who is new to Zig start trying some things out with errors and hit this exact issue. He asked me about why this wouldn’t work, and I didn’t have a good answer. I honestly thought it would as well. This example I have below is definitely contrived, but I think it captures the point.
const ParseError = error{ Invalid, Incomplete, BadCrc };
fn checkSomething(buf: []const u8) !void {
// do stuff
if (condition) return ParseError.BadCrc;
}
fn parser() ParseError!void {
// do some parsing stuff
checkSomething() catch |err| switch |err| {
error.BadCrc => { }, // handle the error
// why do I have to handle the entire ParseError error set when its clear
// that ParseError.BadCrc is the only possible error that could be returned here.
}
// do more stuff
}
If you ignore the bad supporting example, I think its reasonable to expect the compiler to know that, if a function returns only ParseError.BadCrc, its error set is error{BadCrc}. Its actually the entire ParseError set which means if you call checkSomething(), the caller must handle all errors either with an else => unreachable or call out the full error set even though its impossible for other errors to exist. Later down the line, if another error gets added to checkSomething() the else will swallow it possibly introducing a subtle bug. If the error was error{BadCrc}, then any changes to the checkSomething() error possibilities would get caught at compile time.
This behavior is doable now, but you have to split up the errors into smaller subsets. Having a lot of potentially smaller error sets to manage this behavior is a little clunky. Because we already have ! for inferring error sets it seems like a much nicer way to handle some code is to declare a larger error set for your struct/function/file at the top. Each function could then specify which errors of the larger error sets it could produce either explicitly or inferred with !. Like in the above example, I think it would then be possible to explicitly handle subsets of a larger error set much better.
You can always type your error value using error.BadCrc instead of ParseError.BadCrc.
If you have some reason to prefer to use ParseError.BadCrc, you also could create a small helper that turns it into the minimal error set:
const ParseError = error{ Invalid, Incomplete, BadCrc };
inline fn minimalErrorSet(error_value: anytype) @TypeOf(@field(anyerror, @errorName(error_value))) {
return @field(anyerror, @errorName(error_value));
}
fn checkSomething(_: []const u8) !void {
// do stuff
if (true) return minimalErrorSet(ParseError.BadCrc);
}
fn parser() ParseError!void {
// do some parsing stuff
checkSomething("bla") catch |err| switch (err) {
error.BadCrc => {}, // handle the error
// why do I have to handle the entire ParseError error set when its clear
// that ParseError.BadCrc is the only possible error that could be returned here.
};
// do more stuff
}
pub fn main() !void {
try parser();
}
const std = @import("std");
I can’t, I make typos all the time.