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.