Search code examples
rusterror-handlingstack-trace

Get details of multiple-propagated errors in Rust?


I can see several questions about propagating errors in Rust, but none really is about what I'm trying to achieve.

Developing this first small Rust project I've got into the habit of having this return signature:

-> Result<[something from success], Box<dyn std::error::Error>> {
    ...

... and using the ? operator as much as possible in the bodies of these methods.

But now I've found that it sometimes gets difficult to work out where an error is coming from, even when using backtrace::Backtrace. In fact this is a PyO3 project, so the main "entry point" looks like this:

#[pyfunction]
fn index_documents(py: Python, dir_root_path_str: String, index_name: String, op_type: String, app_version_str: String, 
    index_version_str: String, option_suspended_framework: Option<HandlingFramework>) 
    -> PyResult<(usize, usize, Option<HandlingFramework>)> {
    py.allow_threads(move || {
        match index_docs_rust(dir_root_path_str, index_name, op_type, app_version_str, index_version_str, option_suspended_framework) {
            Ok(three_tuple) => Ok(three_tuple),
            Err(e) => {
                error!("backtrace:\n{}", Backtrace::force_capture());
                Err(PyErr::new::<PyTypeError, _>(e.to_string()))
            }
        }
    })
}

fn index_docs_rust( ... 
    -> Result<(usize, usize, Option<HandlingFramework>), Box<dyn std::error::Error>> {
    // makes structs, which call multiple methods, create other structs, which call their
    // own multiple methods, etc.

A lot of lines are produced in force_capture, but none actually gives you the stack trace below (i.e. after) the lines in index_documents, even when doing a development compilation.

As far as I understand things (not much) I believe that, unlike with Python, trace information does not in fact travel with the error object.

I found this answer, which may be as good a solution as possible. But the trouble with that is that (it appears) you have to put the "anticipating" code everywhere where an error might originate... which kind of defeats the object: you might as well use a match block and document things in the Err branch. But in fact that wouldn't be comprehensively useful: a truly useful stack trace will show you (as in Python) exactly the full path which the code has taken through multiple methods before the error happened.

Is there some way to do this in Rust?


Solution

  • Thanks to kmdreko for the solution. Here are the details.

    In Cargo.toml:

    [dependencies]
    anyhow = { version = "*", features = ["backtrace"] }
    
    [profile.release]
    debug = 1
    

    NB I believe there is a penalty to be paid for setting debug to 1 in your release profile like this: either in terms of compile time, or in terms of execution time or in terms of memory usage. An expert might like to edit this to give precise details.

    Add a permanent or temporary environment variable:

    (SET) RUST_BACKTRACE=1
    

    (NB there may be other ways to switch on backtracing...)

    In each file:

    use anyhow::{anyhow, Result};
    

    ... this appears to overwrite std::result::Result for the duration of that file. So now you have to use Result with only one type parameter. (Unless you make the choice of the other one explicit of course: std::result::Result).

    Typical method signature:

    fn do_something(&self, param: String) -> Result<()> {
       ...
    

    You can then use ? in the body of that method with appropriate function calls.

    You can "wrap" a String or an Error as follows (note the exclamation mark: this is a macro):

    return Err(anyhow!(msg))
    

    or

    return Err(anyhow!(e))
    

    or (std::sync::Mutex::lock() is an example of a method with a slightly difficult return value):

    let mut vec = my_mutex.lock().map_err(|_| anyhow!("lock poisoned!"))?;
    

    In the function(s) at the top of the call paths:

    ...
    let _ = match self.do_something(my_string) {
        Ok(()) => Ok(()),
        Err(e) => {
            error!("e {:#?}\ne.backtrace():\n{}", e, e.backtrace());
            Err(e)
        }
    }
    

    This then prints the full stack trace to the error line, including line numbers. Sometimes the actual line number where the error occurred is included but sometimes the line where the call to that method occurs is the last line given in the call path (I haven't worked out what determines which of the two: either way this gives a proper full-call-path stack trace, essential!).

    Also, I found no way to "convert" a Box<dyn std::error::Error> to an anyhow::Error. So the entire module really had to be switched over to using the anyhow way of doing things.