The Missing Piece in Rust Error Handling
Rust offers strong error handling features like explicit control flow, errors as values, and concise propagation using the ?. operator. However, there is a challenge when deciding what to include in the error half of Result. Developers often face a trade-off between precise types that require extra boilerplate and convenient types that hide potential error sources. The key is to make error types compose as easily as the functions returning them.
Consider reading a server port from a file. Reading can fail with an io::Error, while parsing can fail with a ParseIntError. A typical implementation might use the thiserror crate to handle these errors. Nevertheless, deciding how the resulting enum relates to other error enums in the program remains a decision point.
One solution is to use eros, a library that allows expressing a collection of possible error types as an error set. For the port example, eros simplifies the code by eliminating the need for a new enum or conversions. The ErrorUnion enum (io::Error, ParseIntError) holds one of the listed errors, and the tuple describes the possible types without storing both errors.
To further simplify error handling, eros provides .union() and .widen() methods. .union() wraps an ordinary Result's error in an ErrorUnion, inferring the destination set from the surrounding code. This ensures that an accidental propagation of a non-included error results in a compile-time error. .widen() converts an existing union into a union containing all possible errors, ensuring that callers are aware of all potential errors.
Context can also be added to the error set, indicating which operation failed. When additional operations are introduced, their possible errors can be added to the set, and the compiler will check that all possibilities are accounted for. This approach enables precise error handling while avoiding the need for wrapping each function's errors in additional layers of enums.
More importantly, when an error is handled and removed from the set, the return type only contains the remaining error types. This means that a recovery policy can be implemented, where certain errors are transformed into success or a default value, while other errors pass through unchanged. This approach allows the caller to focus on handling the specific error types relevant to their own context, without needing to know about errors that have already been handled lower in the call stack.
The choice of using precise error types or opaque errors can be made at each boundary, depending on whether callers need to make decisions based on the error type or simply need to propagate the failure. Precise error types provide more information for making decisions, while opaque errors allow for simpler propagation. By combining precise and opaque errors appropriately, developers can strike a balance between informative error handling and convenient error propagation.
Written by urgent.news from Lobsters's reporting — not their text. Machine-written — may contain errors; check the original before relying on it.