> literally just "errors are nullable strings returned by functions, and you have to check for them"
That’s definitely not how errors work in Go, either in theory or practice.
In Go, I would characterize errors as an open union type. That’s how I think about them. They turn into strings when you log them.
There is the type of error you get from errors.New. This is not the same as a string, because it is compared using pointer equality, not string equality. For example,
n, err := f.Read(buf)
if err == io.EOF {
// ...
}
This is not a string comparison.
> …errors can't be matched…
You use ==, type assertion, typeswitch, or errors.Is/As/Unwrap is how you match errors in Go. Occasionally you will see a helper function (e.g. os.IsNotExist) but I think these functions are only there because they predate the errors.Is/As/Unwrap interface.
> nor does the type system enforce checking for an error before accessing a value
This sounds like some kind of dogmatic analysis not founded in PL theory. PL theory does not say “it is better for the type system to enforce checking”, because PL theory is not prescriptive.
> adding context to errors is an absolute clusterfuck […] "failed: op failed: encountered error: service could not load foo: error: database error: EAGAIN".
The nice thing about that is that you can then just do this:
if errors.Is(err, syscall.EAGAIN) {
// handle EAGAIN
}
Admittedly, producing good errors requires the programmer to care, and most programmers simply don’t (an observation). The error you wrote is not what errors in the applications
I work with look like. I wrap errors with context when the context is relevant, unwrap context from errors if the context is implicitly understood.
Equally, you will find that programmers don’t care in other languages, like Java, Haskell, or Ruby. When a programmer doesn’t care about errors in Java, you get unreadable stack traces, devoid of context. When a programmer doesn’t care about errors in Haskell, you get partial functions, which throw exceptions (which must be caught in the IO monad) even though the functions are declared to be pure.
Go’s outcomes here seem pretty good to me.
> You say Go has learned some lesson from Java's checked exceptions, because ... errors are return values. What does that have to do with checked exceptions?
The default location to handle exceptions is at a distance. The default location to handle error return values is where they are returned. Checked exceptions are an attempt to bring exception handling closer to the location of the error, but they had ergonomic problems. The ergonomic problems with Java checked exceptions have been discussed elsewhere.
> …I fail to see how picking one approach - of the two only approaches that any language can pick, both of which countless languages have used - is evidence of some kind of deep thought and originality.
“Originality” is what you want to see in research languages like Haskell. That’s the frontier where new ideas are tested. However, speaking as a longtime Haskell programmer, it can often be damn hard to get work done in Haskell. Same thing with Rust—lots of good ideas, can be hard to get work done.
Go is a more conservative approach. “Conservative” does not mean “better” and nor does it mean “worse”, it’s just another niche for programming languages.