Crate iex

Source
Expand description

Idiomatic exceptions.

Speed up the happy path of your Result-based functions by seamlessly using exceptions for error propagation.

§Crash course

Stick #[iex] on all the functions that return Result to make them return an efficiently propagatable #[iex] Result, apply ? just like usual, and occasionally call .into_result() when you need a real Result. It’s that intuitive.

Compared to an algebraic Result, #[iex] Result is asymmetric: it sacrifices the performance of error handling, and in return:

  • Gets rid of branching in the happy path,
  • Reduces memory usage by never explicitly storing the error or the enum discriminant,
  • Enables the compiler to use registers instead of memory when wrapping small objects in Ok,
  • Cleanly separates the happy and unhappy paths in the machine code, resulting in better instruction locality.

§Benchmark

As a demonstration, we have rewritten serde and serde_json to use #[iex] in the deserialization path and used the Rust JSON Benchmark to compare performance. These are the results:

Speed (MB/s) canada citm_catalog twitter
DOM struct DOM struct DOM struct
Result 282.4 404.2 363.8 907.8 301.2 612.4
#[iex] Result 282.4 565.0 439.4 1025.4 317.6 657.8
Performance increase 0% +40% +21% +13% +5% +7%

The data is averaged between 5 runs. The repositories for data reproduction are published on GitHub.

This benchmark only measures the happy path. When triggered, exceptions are significantly slower than algebraic Results. However, it is important to recognize that realistic programs perform actions other than throwing errors, and the slowness of the error path is offset by the increased speed of the happy path. For JSON parsing in particular, the break-even point is 1 error per 30-100k bytes parsed, depending on the data.

Note that just blindly slapping #[iex] onto every single function might not increase your performance at best and will decrease it at worst. Like with every other optimization, it is critical to profile code and measure performance on realistic data.

§Example

use iex::{iex, Outcome};

#[iex]
fn checked_divide(a: u32, b: u32) -> Result<u32, &'static str> {
    if b == 0 {
        // Actually raises a custom panic
        Err("Cannot divide by zero")
    } else {
        // Actually returns a / b directly
        Ok(a / b)
    }
}

#[iex]
fn checked_divide_by_many_numbers(a: u32, bs: &[u32]) -> Result<Vec<u32>, &'static str> {
    let mut results = Vec::new();
    for &b in bs {
        // Actually lets the panic bubble
        results.push(checked_divide(a, b)?);
    }
    Ok(results)
}

fn main() {
    // Actually catches the panic
    let result = checked_divide_by_many_numbers(5, &[1, 2, 3, 0]).into_result();
    assert_eq!(result, Err("Cannot divide by zero"));
}

§All you need to know

Functions marked #[iex] are supposed to return a Result<T, E> in their definition. The macro rewrites them to return an opaque type #[iex] Result<T, E> instead. This type implements Outcome, so you can call methods like map_err, but other than that, you must immediately propagate the error via ?.

Alternatively, you can cast it to a Result via .into_result(). This is the only way to avoid immediate propagation.

Doing anything else to the return value, e.g. storing it in a variable and using it later will not cause UB, but will not work the way you think either. If you want to swallow the error, use let _ = func().into_result(); instead.

Directly returning an #[iex] Result (obtained from a function call) from another #[iex] function also works, provided that it’s the only return statement in the function. Use Ok(..?) if there are multiple returns.

#[iex] works on methods. If applied to a function in an impl Trait for Type block, the corresponding function in the trait Trait block should also be marked with #[iex]. Such traits are not object-safe, unless the method is restricted to where Self: Sized (open an issue if you want me to spend time developing a workaround).

Modules§

example
Examples of rendered documentation for #[iex] functions.

Macros§

try_block
Try block.

Traits§

Contextanyhow
anyhow compatibility layer.
Outcome
Properties of a generalized result type.

Attribute Macros§

iex
Use unwinding for error propagation.