Error values in Zig can be constructed in three ways:
@errorFromInt. This is just a low-level primitive which is useful in niche scenarios (usually ABI stuff, such as passing an error as user data through a C API).
error.Foo. This is by far the most common way.
ErrorSet.Foo, where ErrorSet is an error set type. This is used incredibly rarely.
It's clear that ErrorSet.Foo is theoretically a redundant form, because it's exactly identical to @as(ErrorSet, error.Foo). The type annotation in that alternative is only relevant in very niche scenarios; in virtually all cases, you can get away with just writing error.Foo instead. In practice, people almost exclusively write error.Foo instead of ErrorSet.Foo; to the point where I encounter the latter so rarely it briefly confuses me whenever I do. As is often the case, having two ways to do the same thing hinders readability.
In principle, ErrorSet.Foo could have the benefit of resistance against typos, because accidentally writing ErrorSet.Fooo is a compile error, while error.Fooo is not. However, I don't find this argument compelling. There are two common scenarios where writing error values: in switch items (by which I mean the values to the left of =>), and in return <error value>. In both of these cases, there is already a specific error type which is expected. In the latter case, the function could have an inferred error set (i.e. !T instead of ErrorSet!T), but if a user is going to go to the effort of writing return ErrorSet.Foo everywhere, it's more valuable to redirect that effort to the return type, where all return values are guaranteed to be checked, the code becomes more readable, and generated documentation is improved.
Lastly, here are two more annoying consequences of ErrorSet.Foo:
- They introduce a special case to Decl Literals. Usually,
.Foo is equivalent to ResultType.Foo, but this is not the case for error sets (you can't write .MyError as shorthand for ResultErrorSet.MyError). This shows up as an explicit special case in the compiler implementation.
- They allow constructing error values at comptime with
@field (for instance, @field(anyerror, my_string)). This isn't used in practice, but could hugely harm readability of a codebase if it was (e.g. it could become very difficult to find where an error was returned from).
As a result of the above, I propose disallowing ErrorSet.Foo (including anyerror.Foo) in favour of error.Foo. This would also disallow @field(ErrorSet, "foo") (including @field(anyerror, "foo")), so it becomes impossible to construct error values at comptime from a string error name.
Error values in Zig can be constructed in three ways:
@errorFromInt. This is just a low-level primitive which is useful in niche scenarios (usually ABI stuff, such as passing an error as user data through a C API).error.Foo. This is by far the most common way.ErrorSet.Foo, whereErrorSetis an error set type. This is used incredibly rarely.It's clear that
ErrorSet.Foois theoretically a redundant form, because it's exactly identical to@as(ErrorSet, error.Foo). The type annotation in that alternative is only relevant in very niche scenarios; in virtually all cases, you can get away with just writingerror.Fooinstead. In practice, people almost exclusively writeerror.Fooinstead ofErrorSet.Foo; to the point where I encounter the latter so rarely it briefly confuses me whenever I do. As is often the case, having two ways to do the same thing hinders readability.In principle,
ErrorSet.Foocould have the benefit of resistance against typos, because accidentally writingErrorSet.Fooois a compile error, whileerror.Fooois not. However, I don't find this argument compelling. There are two common scenarios where writing error values: inswitchitems (by which I mean the values to the left of=>), and inreturn <error value>. In both of these cases, there is already a specific error type which is expected. In the latter case, the function could have an inferred error set (i.e.!Tinstead ofErrorSet!T), but if a user is going to go to the effort of writingreturn ErrorSet.Fooeverywhere, it's more valuable to redirect that effort to the return type, where all return values are guaranteed to be checked, the code becomes more readable, and generated documentation is improved.Lastly, here are two more annoying consequences of
ErrorSet.Foo:.Foois equivalent toResultType.Foo, but this is not the case for error sets (you can't write.MyErroras shorthand forResultErrorSet.MyError). This shows up as an explicit special case in the compiler implementation.@field(for instance,@field(anyerror, my_string)). This isn't used in practice, but could hugely harm readability of a codebase if it was (e.g. it could become very difficult to find where an error was returned from).As a result of the above, I propose disallowing
ErrorSet.Foo(includinganyerror.Foo) in favour oferror.Foo. This would also disallow@field(ErrorSet, "foo")(including@field(anyerror, "foo")), so it becomes impossible to construct error values at comptime from a string error name.