> There really was a strong consensus about exceptions, and then an agreement that checked exceptions are a failure, and now, suddenly, we are back to “checked exceptions” with a twist, in the form of “errors are values” philosophy.
I think the key difference between error values and checked exceptions is that functions in an error-value language tend to fully own their error types. With checked exceptions, if foo() calls bar() and then bar's implementation changes to add a new exception, suddenly foo is throwing stuff it didn't know about. With error values, foo is never simply throwing whatever bar throws, because foo has to explicitly transform bar's error value into its own error type if it wants to bubble it up.
It's a subtle change—perhaps a purely cultural change, even—but the difference is obvious if you picture a Java method assuming responsibility for its errors the way Rust functions do. Just imagine someone writing `try { bar(); } catch (BarError f) { raise FooError::fromBarError(f); }`. It's weird. It isn't done. But `bar().map_err(|e| FooError::fromBarError(e))?` is completely idiomatic in Rust.
It helps that defining a new type is low-ceremony in Rust, whereas it's high-ceremony in Java. I think that has a lot of benefits. When we model everything with types, it's important that types be cheap.
> On the one hand, at lower-levels you want to exhaustively enumerate errors...
> On the other hand, at higher-levels, you want to string together widely different functionality from many separate subsystems without worrying about specific errors...
I feel like the Rust ecosystem of crates has naturally grown to handle these two ideas pretty well. `anyhow` for applications, `thiserror` for libraries.
I think the key difference between error values and checked exceptions is that functions in an error-value language tend to fully own their error types. With checked exceptions, if foo() calls bar() and then bar's implementation changes to add a new exception, suddenly foo is throwing stuff it didn't know about. With error values, foo is never simply throwing whatever bar throws, because foo has to explicitly transform bar's error value into its own error type if it wants to bubble it up.
It's a subtle change—perhaps a purely cultural change, even—but the difference is obvious if you picture a Java method assuming responsibility for its errors the way Rust functions do. Just imagine someone writing `try { bar(); } catch (BarError f) { raise FooError::fromBarError(f); }`. It's weird. It isn't done. But `bar().map_err(|e| FooError::fromBarError(e))?` is completely idiomatic in Rust.
It helps that defining a new type is low-ceremony in Rust, whereas it's high-ceremony in Java. I think that has a lot of benefits. When we model everything with types, it's important that types be cheap.
reply