In a language that takes error handling seriously, you have domain types for errors, type checking for exhaustive error handling, and you get info like a stack trace out of the box.
In a language that takes error handling seriously, you have domain types for errors, type checking for exhaustive error handling, and you get info like a stack trace out of the box.
There is nothing special about an error in Go, and there is no type hierarchy to anchor them against anyway. An error can be a struct{} carrying no value at all, or simple integer with no way to carry context, etc. Error-wrapping is the solution that has been devised to work within these constraints, but there's no obvious way to compose that with the various ideas around reducing boilerplate. For a point of comparison, Rust's anyhow crate was able to square this circle by adding a blanket trait impl for Context, but Go has no equivalent for that. (Also, Rust's ? operator is implemented using unstable components, allowing the details to evolve even while the feature itself is usable in stable Rust, something which also has no equivalent in Go).
As to the lack of discriminated unions, I agree wholeheartedly. There have been some proposals recently to get them into the language, with various levels of conservatism and cohesion with the rest of the language, but a lot of them are faltering on the "every type must have a well defined zero value" issue (yet another design decision that doesn't necessarily have anything to do with errors).
I had to argue about this years ago with a coding standard pushing to define local variables at the top of functions and zero initialize them in C. Why default to null pointers or worse (invalid ints/structures) when you could make use of uninitialized variables be a compiler error instead!?
That may seem out of place in the world you live in, but Go was created in a world where Python, Ruby, and Javascript were the languages all the cool kids were using. Go was intended to be the language for C++ programmers who had to use C++ for performance reasons but wished they could use Python instead.
Insanity, perhaps, but Go was designed to solve a problem for the world it lived in. You don't get to choose your circumstances. For those living in paradise, they can use the languages designed for their world.
Personally, I'm fine with discriminated unions always allowing "nil" in Go, since IMO the most sensible way to implement them anyway is as a special case of interfaces. Not everyone feels the same way, though.