I think you shouldn’t worry too much about the E.A syntax since it might be going away, although the Zig team hasn’t decided yet.
Although error{ A, B }!noreturn and error{ A, B } are distinct types, they are effectively equivalent: each type has the same number of possible values. In fact, the choice to use an error union type as the return type of foo is arbitrary, and the following code is valid:
const E = error{ A, B };
fn foo(b: bool) E {
return if (b) E.A else while (true) {};
}
If we take a look at resolvePeerTypesInner and PeerResolveStrategy in src/Sema.zig (if Codeberg lets you load it), we can see that peer types are resolved (roughly) according to the following order of precedence:
noreturn + T (any type) = T
@TypeOf(undefined) + T = T
error{A} + error{B} = error{A,B}
error{A} + error{B}!T = error{A,B}!T
error{A} + T = error{A}!T
error{A}!T + error{B}!T = error{A,B}!T
…(draw the rest of the owl)…
The noreturn + T resolution pair has the highest precedence of all, so therefore error{A} + noreturn favors error{A} over error{A}!noreturn. This is easy to reason with and does not feel contradictory or unexpected to me.
Had the order of precedence been swapped around and error sets prioritized, then the following code
might have resulted in a compile error like expected type 'error{A,B}', found 'error{A,B}!noreturn'. I think this outcome would have felt less intuitive than status quo.
I’m wondering whether or not noreturn should be allowed to use as the payload type of error unions. Currently, I haven’t found a meaningful use case of ErrSet ! noreturn.