Crate outcome[−][src]
Expand description
Outcome<S, M, F>
is an augmentation of the Result
type
found in the Rust standard library.
It is an enum with the variants
Success(S)
, representing success and containing a valueMistake(M)
, representing an optionally retryable error and containing a valueFailure(F)
, representing failure and containing a value.
enum Outcome<S, M, F> { Success(S), Mistake(M), Failure(F), }
Outcome
is an augmentation to Result
. It adds a third state to
the “success or failure” dichotomy that Result<T, E>
models. This third
state is that of a soft or retryable error. A retryable error is one
where an operation might not have succeeded, either due to other operations
(e.g., a disk read or write not completing), misconfiguration (e.g.,
forgetting to set a specific flag before calling a function), or busy
resources (e.g., attempting to lock an audio, video, or database resource).
no_std
support
// TODO: …
Why Augment Result<T, E>
?
Outcome
is not intended to fully replace Result
, especially at
the API boundary (i.e., the API used by clients) when there is a clear
success of failure state that can be transferred to users. Instead, it
provides the ability to quickly expand the surface area of consumed APIs
with finer grained control over errors so that library writers can write
correct behavior and then return at a later time to compose results,
expand error definitions, or to represent different error severities.
As an example, the section making unhandled errors unrepresentable in the post Error Handling in a Correctness-Critical Rust Project, the author states:
this led me to go for what felt like the nuclear solution, but after seeing how many bugs it immediately rooted out by simply refactoring the codebase, I’m convinced that this is the only way to do error handling in systems where we have multiple error handling concerns in Rust today.
The solution, as they explain in the next paragraph is
make the global
Error
enum specifically only hold errors that should cause the overall system to halt - reserved for situations that require human intervention. Keep errors which relate to separate concerns in totally separate error types. By keeping errors that must be handled separately in their own types, we reduce the chance that the try?
operator will accidentally push a local concern into a caller that can’t deal with it.
As the author of this post later shows, the Sled::compare_and_swap
function returns a Result<Result<(), CompareAndSwapError>, sled::Error>
.
They state this looks “way less cute”, but will
improve chances that users will properly handle their compare and swap-related errors properly[sic]
ⓘ// we can actually use try `?` now let cas_result = sled.compare_and_swap( "dogs", "pickles", "catfood" )?; if let Err(cas_error) = cas_result { // handle expected issue }
The issue with this return type is that there is technically nothing to
stop a user from using what the creator of this crate calls the WTF
operator (??
) to ignore these intermediate errors.
let cas = sled.compare_and_swap("dogs", "pickles", "catfood")??;
It would be hard to forbid this kind of usage with tools like clippy due to libraries such as nom relying on nested results and expecting moderately complex pattern matching to extract relevant information.
Luckily, it is easier to prevent this issue in the first place if:
- An explicit call to extract an inner
Result<T, E>
must be made - The call of an easily greppable/searchable function before using the
“WTF” (
??
) operator is permitted. - The
Try
orTryV2
trait returns a type that must be decomposed explicitly and does not support the try?
operator itself.
Thanks to clippy’s
disallowed_method
lint, users can rely on the first two options until
TryV2
has been stabilized.
State Escalation
// TODO: …
XXX: The section below is to be rewritten. Ignore it for now.
This statement above is exactly what Outcome
is written to help
prevent.
As
an example, UnixDatagram::take_error
returns a type that, once expanded
looks something like much like Result<Option<Error>, Error>
. Internally,
this is calling a series of internal functions that eventually call into
afunction that is part of the C FFI. However, there are two error states
here. The actual possible error that users will care about (the
Option<Error>
) and the Error
that might be returned for a variety of
other reasons.
In other words,
Outcome
is not for the average Rust developer to use, but for
internal use within the confines of a crate.
The idea behind an Outcome
is that error handling cannot usually signify
to the client of an API that a function should be retried. In some Rust
libraries, the use of a Result<T, Result<U, E>>
is used in these places
and it makes consuming these APIs cumbersome, unnecessarily difficult,
and promotes use of the WTF operator (??
).
#[derive(Debug)] enum Version { V1, V2 } struct EmptyInput; fn parse_version(header: &[u8]) -> Outcome<Version, EmptyInput, &'static str> { match header.get(0) { None => Mistake(EmptyInput), Some(&1) => Success(Version::V1), Some(&2) => Success(Version::V2), Some(_) => Failure("invalid or unknown version"), } } let _version = parse_version(&[]);
In other cases, such as the one found with TryLockError<T>
, the
non-recoverable error is PoisonError<T>
, while the retryable error
returned is WouldBlock
, which can be tried again, possibly with a
pattern known as exponential back-off.
Structs
Enums
Traits
An attempted conversion that consumes self
, which may or may not be
expensive. Outcome’s analogue to TryInto
.