Search code examples
methodserror-handlingrustclosureslazy-evaluation

What is the difference between "context" and "with_context" in anyhow?


This is the documentation for anyhow's Context:

/// Wrap the error value with additional context.
fn context<C>(self, context: C) -> Result<T, Error>
where
    C: Display + Send + Sync + 'static; 
/// Wrap the error value with additional context that is evaluated lazily
/// only once an error does occur.
fn with_context<C, F>(self, f: F) -> Result<T, Error>
where
    C: Display + Send + Sync + 'static,
    F: FnOnce() -> C;

In practice, the difference is that with_context requires a closure, as shown in anyhow's README:

use anyhow::{Context, Result};

fn main() -> Result<()> {
    // ...
    it.detach().context("Failed to detach the important thing")?;

    let content = std::fs::read(path)
        .with_context(|| format!("Failed to read instrs from {}", path))?;
    // ...
}

But it looks like I can replace the with_context method with context, get rid of the closure by deleting ||, and the behaviour of the program wouldn't change.

What is the difference between the two methods under the hood?


Solution

  • The closure provided to with_context is evaluated lazily, and the reason you'd use with_context over context is the same reason you'd choose to lazily evaluate anything: it rarely happens and it's expensive to compute. Once those conditions are satisfied then with_context becomes preferable over context. Commented pseudo-example:

    fn calculate_expensive_context() -> Result<()> {
        // really expensive
        std::thread::sleep(std::time::Duration::from_secs(1));
        todo!()
    }
    
    // eagerly evaluated expensive context
    // this function ALWAYS takes 1+ seconds to execute
    // consistently terrible performance
    fn failable_operation_eager_context(some_struct: Struct) -> Result<()> {
        some_struct
            .some_failable_action()
            .context(calculate_expensive_context())
    }
    
    // lazily evaluated expensive context
    // function returns instantly, only takes 1+ seconds on failure
    // great performance for average case, only terrible performance on error cases
    fn failable_operation_lazy_context(some_struct: Struct) -> Result<()> {
        some_struct
            .some_failable_action()
            .with_context(|| calculate_expensive_context())
    }