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 value
  • Mistake(M), representing an optionally retryable error and containing a value
  • Failure(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 or TryV2 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

An iterator over the value in a Success variant of an Outcome.

An iterator over a reference to the Success variant of an Outcome.

An iterator over a mutable reference to the Success variant of an Outcome.

Enums

Aberration is a type that can represet a Mistake, or Failure.

(WIP Name) Concern is a type that can represent a Success, or Mistake.

Outcome is a type that can represet a Success, Mistake, or Failure.

Traits

Outcome’s analogue to TryFrom, and the reciprocal of TryInto.

An attempted conversion that consumes self, which may or may not be expensive. Outcome’s analogue to TryInto.