dtolnay/anyhow

Replacing the stack trace of an anyhow error

nipunn1313 opened this issue · 3 comments

Hello. My company has a use case where we'd like to replace the stack trace of an anyhow while maintaining the context.

As a motivating use case. I left out a lot of irrelevant details to simplify.

struct OrigError;

// spawn a worker
let (input_tx, input_rx) = sync::mpsc::channel();
let (output_tx, output_rx) = sync::mpsc::channel();
thread::spawn(move || {
  loop {
    let input = input_rx.recv();
    // process(input)
    output_tx.send(Err(anyhow::anyhow!(OrigError)));
  }
})

fn do_work(input: Input) -> anyhow::Result<Output> {
  input_tx.send(input);
  let result = output_rx.recv();
  // Here, we would like the stack trace of the receiving side rather
  // than the stack within the worker thread. It's more useful to know
  // which callsite failed rather than the stack inside the worker.
  result
}

I originally thought it might involve using anyhow::anyhow!() which creates a stack trace

if let Err(e) = result && e.is::<OrigError>() {
  return Err(anyhow::anyhow!("New stack trace").context(result));
}

This successfully gives a new stack trace, but loses context.

This rust playground illustrates my confusion here
https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=9e784df20991893689d17155c502b60e
(copy pasted here)

#[derive(thiserror::Error, Debug)]
#[error("It's an OrigError")]
struct OrigError;

fn main() {
    // Works
    let e = anyhow::anyhow!(OrigError);
    assert!(e.is::<OrigError>());
    
    // Works
    let e = anyhow::anyhow!("new").context(OrigError);
    assert!(e.is::<OrigError>());
    
    // Does not work
    let e = anyhow::anyhow!("new").context(anyhow::anyhow!(OrigError));
    assert!(e.is::<OrigError>());
}

I was a little confused by the behavior of .context(another_anyhow) - as it does not appear to transfer the contexts of another_anyhow. I also could not find a way to directly replace the stacktrace on an err (eg err.replace_stacktrace()).

Some poorly formed solution ideas. Open to better ideas too or other suggestions within the existing API.

  • Change anyhow's .context() to specially handle nested anyhow - and transfer over the context stack
  • Add an explicit API for replacing the stacktrace

Thank you! We love anyhow over here.

You can't assign an arbitrary Backtrace, but you can cause a new backtrace to be captured as follows:

use anyhow::anyhow;
use std::env;
use std::error::Error;
use std::fmt::{self, Debug, Display};
use thiserror::Error;

#[derive(Error, Debug)]
#[error("It's an OrigError")]
pub struct OrigError;

struct EraseBacktrace(anyhow::Error);

impl Error for EraseBacktrace {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        self.0.source()
    }

    // no fn backtrace()
}

impl Display for EraseBacktrace {
    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
        Display::fmt(&self.0, formatter)
    }
}

impl Debug for EraseBacktrace {
    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
        Debug::fmt(&self.0, formatter)
    }
}

fn main() {
    env::set_var("RUST_LIB_BACKTRACE", "1");
    let file = file!();

    let (e0, line) = (anyhow!(OrigError).context("Thing failed"), line!());
    dbg!(&e0, format_args!("\n{:?}", e0));
    assert!(e0.backtrace().to_string().contains(&format!("{file}:{line}")));

    let (e1, line) = (anyhow!(EraseBacktrace(e0)), line!());
    dbg!(&e1, format_args!("\n{:?}", e1));
    assert!(e1.backtrace().to_string().contains(&format!("{file}:{line}")));
}

Hi. Thanks for the message! I've implemented this strategy, but it doesn't quite work because e1.is::<OrigError>() or e1.downcast_ref::<OrigError>().is_some() isn't true.

Full code sample (adapted from the one you posted - adding two more asserts).

use anyhow::anyhow;
use std::env;
use std::fmt::{self, Debug, Display};

#[derive(thiserror::Error, Debug)]
#[error("It's an OrigError")]
pub struct OrigError;

struct EraseBacktrace(anyhow::Error);

impl std::error::Error for EraseBacktrace {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        self.0.source()
    }

    // no fn backtrace()
}

impl Display for EraseBacktrace {
    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
        Display::fmt(&self.0, formatter)
    }
}

impl Debug for EraseBacktrace {
    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
        Debug::fmt(&self.0, formatter)
    }
}

fn main() {
    env::set_var("RUST_LIB_BACKTRACE", "1");
    let file = file!();

    let (e0, line) = (anyhow!(OrigError).context("Thing failed"), line!());
    dbg!(&e0, format_args!("\n{:?}", e0));
    assert!(e0
        .backtrace()
        .to_string()
        .contains(&format!("{file}:{line}")));

    let (e1, line) = (anyhow!(EraseBacktrace(e0)), line!());
    dbg!(&e1, format_args!("\n{:?}", e1));
    assert!(e1
        .backtrace()
        .to_string()
        .contains(&format!("{file}:{line}")));

    // these asserts fail.
    assert!(e1.downcast_ref::<OrigError>().is_some());
    assert!(e1.is::<OrigError>());
}

I see — I think you're out of luck. You would need to use a different library.