Expand description

Overview

Audit Pull Request codecov Documentation

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).

use outcome::prelude::*;

#[derive(Debug, PartialEq)]
enum Version { V1, V2 }

#[derive(Debug, PartialEq)]
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(&[]);
assert_eq!(version, Mistake(EmptyInput));

Usage

At this time, the name outcome is already taken on crates.io. As crates.io does not yet support namespaces or collections, we’ve had to take a unique approach to still publish the crate. To do this, we’ve generated a UUIDv5 string via python:

from uuid import *
print(uuid5(uuid5(NAMESPACE_DNS, "occult.work"), "outcome"))

This should generate the string 46f94afc-026f-5511-9d7e-7d1fd495fb5c. Thus the dependency in your Cargo.toml will look something like:

[dependencies]
outcome-46f94afc-026f-5511-9d7e-7d1fd495fb5c = "*"

However, the exported library is still named outcome, so importing it is treated the same:

use outcome::prelude::*;

Users can also work around this by using the package key in their dependency declaration:

[dependencies.outcome]
version = "*"
package = "outcome-46f94afc-026f-5511-9d7e-7d1fd495fb5c"

Is this solution friendly to users? No, but neither is the lack of namespacing nor a squatting policy on crates.io. If/when this problem is resolved, this crate’s documentation (and name!) will be changed and all versions will be yanked.

Features

There are several features available to the crate that are disabled by default. These include:

  • unstable (Enable “unstable” functions that mirror unstable functions found in Result. Unlike Result, however, a nightly compiler is not required.)
  • nightly (Enable features that require the nightly rust compiler to be used, such as Try)
  • report (Enable conversion from Aberration to an eyre::Report)
  • diagnostic (Enable conversion from Aberration to a miette::Report)

Users can also enable no_std support by either setting default-features to false or simply not listing std in the list of features. Lastly, the report and diagnostic features should be treated as mutually exclusive as they both export a similar interface that is similar enough to cause collisions. If the author of eyre ever manages to make it no_std (there are limitations preventing Error from being no_std), these features will be redesigned such that report + std uses miette, and report + no_std uses eyre.

  • nightly will enable unstable.
  • report will enable std.
  • diagnostic will enable std.

NOTE: Due to limitations with cargo features, we cannot actually enforce mutual exclusivity, and support building the crate with documentation and tests with all features enabled.

no_std

Nearly every single feature in outcome supports working with #![no_std] support, however currently eyre does require std support (Attempts were made at making no_std work, but this was removed and has not been available for some time).

[dependencies.outcome]
package = "outcome-46f94afc-026f-5511-9d7e-7d1fd495fb5c"
version = "..."
features = ["nightly"]

unstable

When enabled, the unstable feature provides several associated methods for Outcome that mirror unstable APIs found in Result<T, E>. If the methods mirrored are changed in any future releases of stable rust, these will as well. Additionally, if any of the APIs are stabilized, they will be moved out of this feature and into the default feature set. Unlike the nightly feature, these APIs can be implemented in stable rust.

nightly

The nightly feature set also requires a nightly toolchain. This is detected in outcome’s build.rs script via the rustversion crate. While users can enable the nightly feature on a stable toolchain, nothing additional will be compiled.

Once available, users will have to enable specific nightly features for each API set mentioned. These are listed below.

  • #![feature(try_trait_v2)] — operator ? support
    • Outcome may be used with operator ?, including from functions that return a Result<T, E>, as long as:
      • E: From<Outcome::Failure>
  • #![feature(never_type)] — APIs that return !
    • Outcome will have several functions where the ! type is used in the function signature. These include into_success, and others.
    • Several stable functions that return an Aberration will instead return an Outcome<!, M, F>.
  • #![feature(termination_trait_lib)] — Exit process with an Outcome
    • NOTE: This requires the std feature to be enabled as well.
    • In addition to being usable with fn main(), any unit test may return an Outcome directly. This works in the same way as returning a Result<T, E>

report

The report feature adds the WrapFailure trait to both Outcome and Aberration. This trait is meant to mimic the WrapErr trait found on Result<T, E> that is provided by eyre. Therefore, a blanket implementation is provided for all types that implement WrapErr. However, to stay in line with outcome’s naming convention, instances of err have been replaced with failure.

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 or 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::Tree::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 the outcome 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>-like type must be made
  • The call of an easily greppable/searchable function before using the “WTF” (??) operator is permitted.
  • The Try 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 Try has been stabilized.

outcome provides this in the form of its Concern type, whose variants match the Success and Failure of Outcome, as well as the associated function Outcome::acclimate, which returns a Result<Concern<S, M>, F>.

NOTE: This associated function will be deprecated once Try has been stabilized.

State Escalation

Using the parse_version example seen earlier in our documentation, we can see that the Version enum can return either V1 or V2. At some point, a tool parsing some version number might choose to deprecate support for V1. In these instances, a developer might choose to turn V1 into a Mistake. However, parse_version returns a Success(V1). How, then, can we turn this into a Mistake in-place?

This is where state escalation comes into play. It is one of the more experimental features that outcome is trying to experiment with. The basic idea of this concept is a successful outcome for one function does not imply that the caller considers the successful value valid enough to continue.

With state escalation, we not only move the possible state of an Outcome from Success to Mistake or Mistake to Failure, but we are also able to eliminate possible states from the Outcome. That is, given an Outcome<S, M, F>, the first state escalation would return an Outcome<!, M, F> (on stable, this returns an Outcome<Infallible, M, F>. A second state escalation would result in an Outcome<!, !, F>. This allows for fast state transitions with little to no effort, while keeping this state transition separate from mapping operations. The benefit of reducing the possible set of states that the Outcome can represent is simply a side effect of Rust’s powerful type system with regards to enums and their variants.

It is important to note that there is no de-escalation of state possible without explicit operations from users. In other words, a new Outcome must be generated when one or more fields are Infallible. This is difficult to enforce without certain features missing from Rust, specifically partial specialization and impl overloading (e.g., an Outcome<!, M, F> should not be allowed to have and_then called on it). However this same limitation is found in Result and other variants, and even Finite State Machines implemented with Rust enums will suffer the same fate. In an ideal world, Outcome’s state escalation would only permit unidirectional transitions, and would require the creation of a completely new Outcome if a user wanted to reset or de-escalate the Outcome. This might not ever be possible to do or enforce in Rust, so in the meantime it is up to users to behave themselves with regards to escalating state.

Modules

Traits for retryable conversions between types.

diagnosticdiagnostic

Support for the miette crate.

The Outcome Prelude

reportreport

Support for the eyre crate.

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 represent a Mistake, or Failure.

Concern is a type that can represent a Success, or Mistake.

Outcome is a type that represents 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.