The Second Great Error Model Convergence

69 points by carlana


scraps

Love this. I especially love the admission that checked exceptions are back.

There's an aspect of propagation that I think is missing from this piece.

In Rust, we have the ? operator, but that so rarely acts as a satisfactory error chaining device that can give you breadcrumbs to where something has actually happened, thus we have things like the context mechanisms in anyhow, and our Result<_, E> values have to transform E into Box<dyn Error> somewhere along the way. Python has raise ... from, and Go has fmt.Errorf. Each of these can be used to provide nicely-formated breadcrumbs to help an operator learn about errors, but each of them has their quirks.

anyhow is type-erasing, so you have downcast_ref to check if a given error matches some type, but if you want to inspect if any error in the whole chain has a given type, that's an exercize left to the reader. anyhow tends to not be used in libraries since it would appear in the library's signature and force the library's consumers to directly (not merely transitively) depend on anyhow, so people tend to use thiserror instead. Again, inspecting chains tends to be fairly tedious, especially if you have N distinct error types that can be caused by 1 concrete error type somewhere along the way.

Python's raise ... from construct can provide a chain not just of values, but of stack traces, which is nice, I suppose, but you're still dealing with exceptions, the pile of stack traces if very noisy, and the except clause has no facility for unwrapping exceptions wrapped this way or inspecting the chain, so inspecting the chain is an exercise left to the reader.

Go's fmt.Errorf does a good job with wrapping, and errors.Is does a good job of inspecting a chain for a matching value. errors.As is a rather cumbersome way for inspecting a chain for a given type, and is sensitive to the distinction between T and *T so can easily lead to finicky distinctions that aren't meaningfully checked by the compiler.

Blindly calling if err != nil { return err } in Go and blindly using Rust's ? are the most convenient thing for the caller, which is why people do them, but very often leave an operator scratching their head as to how an error occurred.

After programming Rust and Go and Python all in production, (and being on call for things made by other engineers in all three languages), I'm left liking Go's error system the most, and Python's the least, with Rust being in the middle. I strongly dislike stack traces as the means of understanding where an error has occurred, so I'm glad that it seems that newer languages are largely eschewing this as the default.