Reemus Icon

Retain 'anyhow' Result Error Context With 'once_cell::Lazy'

👍🔥❤️😂😢😕
views
comments

This little problem griped me so hard when I was creating gitnr.

Consider the following:

  • We use once_cell::Lazy to lazily initialize a static variable
  • The static variable is of type anyhow::Result as that's what we want to return from the Lazy function
  • The Lazy function should it error has additional context or error information we need for debugging

Now by default, if you attempt the code below, should an error occur, the error context will be lost and you will only see a surface level error message.

Are you encountering an error when trying to use a function that returns a Result within a .map() closure in Rust? I'm sure this is a common problem people face when learning to use Rust. Thankfully, the solution is actually quite simple.

Problem

Consider the following Rust code where we have two functions, both returning Result types. The first function prefixes a line (a string slice), and the second function takes a list of lines and prefixes each using the first function.

use std::error::Error;
 
fn prefix_line(line: &str) -> Result<String, Box<dyn Error>> {
    Ok(format!("PREFIX: {}", line))
}
 
fn prefix_each(lines: &[&str]) -> Result<Vec<String>, Box<dyn Error>> {
    let result = lines
        .iter()
        .map(|line| prefix_line(line)?)
        .collect::<Vec<_>>();
 
    Ok(result)
}
rust-icon

Attempting to compile this code results in an error on line 10:

the '?' operator can only be used in a closure that returns 'Result' or 'Option' (or another type that implements 'FromResidual') [E0277] Help: the trait 'FromResidual<Result<Infallible, Box<dyn StdError>>>' is not implemented for 'std::string::String'

Breaking down the error:

  • The map function takes a closure as it's argument
  • The ? operator inside the .map() closure is problematic
  • This is because the ? operator expects the closure to return a Result or Option
  • Instead, the closure's return type is inferred to match the expected output type of map, which is String
  • Since String doesn't implement FromResidual, you get the error

Solutions

Collect the results

The simplest way I've found to solve this issue:

  • Call prefix_line(line) without the ? operator which outputs Vec<Result<...>>
  • Use collect::<Result<Vec<_>, _>>()? to correctly infer types, collect the Results and propagate errors if any
  • Leaving us with our desired output Vec<String>
fn prefix_each(lines: &[&str]) -> Result<Vec<String>, Box<dyn Error>> {
    let result = lines
        .iter()
        .map(|line| prefix_line(line))
        .collect::<Result<Vec<_>, _>>()?;
 
    Ok(result)
}
rust-icon

Using a loop

The obvious initial solution but it has drawbacks such as:

  • It breaks the functional style
  • It requires a mutable variable
fn prefix_each(lines: &[&str]) -> Result<Vec<String>, Box<dyn Error>> {
    let mut result = Vec::new();
 
    for line in lines {
        result.push(prefix_line(line)?);
    }
 
    Ok(result)
}
rust-icon

Using filter_map

  • filter_map discards Err values collecting only successful Ok values
  • Useful if you want to ignore errors and only care about the successful results
fn prefix_each(lines: &[&str]) -> Result<Vec<String>, Box<dyn Error>> {
    let result = lines
        .iter()
        .filter_map(|line| prefix_line(line).ok())
        .collect::<Vec<_>>();
 
    Ok(result)
}
rust-icon

Using try_fold

  • Use try_fold to aggregate results and handle errors gracefully
  • It allows you to return a Result type directly
fn prefix_each(lines: &[&str]) -> Result<String, Box<dyn Error>> {
    let result = lines.iter().try_fold(String::new(), |mut acc, line| {
        prefix_line(line).map(|prefixed_line| {
            acc.push_str(&prefixed_line);
            acc.push_str("\n");
            acc
        })
    })?;
 
    Ok(result)
}
rust-icon
👍🔥❤️😂😢😕

Comments

...

Your name will be displayed publicly

Loading comments...