How not to panic using `Backtrace` in Rust

How not to panic using `Backtrace` in Rust

Alexandre Hanot

In your software, there are expected errors such as YouMustBeEighteenOrOlder or PageNotFound. These are errors that you create and expect to happen sometimes. But there are also unexpected errors such as MongoDbError or UnreachableApi, especially errors from other libraries. These are errors that you don't expect to happen, but they do.

You don't want to create dozens of errors to handle all of your library/unexpected errors. You might consider two options: panic via unwrap or something like Box<dyn Error>.

The first one is basically like saying: "I don't know what to do, I'm out of here". The second one is like saying: "I don't know what to do, but I'll let you know".

🚫

The first one is bad practice for a simple reason: when writing code, the caller should know about the callee. If the callee panics, the caller has no idea what happened. Your program is lying to you. This is bad and you should slap your own hand everytime it happens.

Using a generic Error

The better way of handling the situation is having a generic error type that can handle all of your unexpected errors. There is a crate that does exactly that: anyhow.

use anyhow::Result;
 
// Result is a type alias for Result<T, anyhow::Error>
fn main() -> Result<()> {
    let result = std::fs::read_to_string("foo.txt")?;
    println!("{}", result);
    Ok(())
}

I do not recommend using anyhow because you just exchanged one error type for another one. It's like saying: "I don't know what to do, but I'll let you know with a different message".

Using thiserror

We can do better by defining our own error type using thiserror which will only allows us to quickly implement Display trait via the Error trait.

use std::fmt::Debug;
use thiserror::Error;
 
#[derive(Error, Debug)]
#[error("An unexpected error occurred: {message}")]
pub struct UnexpectedError {
    message: String,
}
⚠️

Problem is: when such an error happen, you want to know where it happened and {message} won't help you much.

Using Backtrace

If we add some Backtrace to our error, we can have a better understanding of where the error happened. Note: I am using the crate backtrace = "0.3.69" and tracing = "0.1.40".

use backtrace::Backtrace;
use tracing::error;
 
impl UnexpectedError {
    pub fn from<T: std::fmt::Display>(err: T) -> Self {
        error!(
            error = %err,
            trace = ?BackTrace::new(),
            "Unexpected Error"
        );
        UnexpectedError {
            message: err.to_string(),
        }
    }
}

A quiet Backtrace

You will notice that Backtrace will output a LOT of information. You might want to quiet it down a bit. I made some custom filters to remove the noise. Especially from the .cargo and rustc directories.

 
const FILTERS: [&str; 3] = [".cargo", "rustc"];
 
struct Trace(Backtrace);
 
impl Trace {
    pub fn new() -> Self {
        Self(backtrace::Backtrace::new())
    }
}
 
impl Debug for Trace {
    fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        // Filter out frames that are not relevant to the user
        let frames = self
            .0
            .frames()
            .iter()
            .filter(|frame| {
                let symbols = frame.symbols();
                for symbol in symbols {
                    if let Some(name) = symbol.filename() {
                        let filename = format!("{:?}", name);
                        if !FILTERS.iter().any(|filter| filename.contains(filter)) {
                            return true;
                        }
                    }
                }
                false
            })
            .collect::<Vec<&BacktraceFrame>>();
 
        // Print the backtrace (see capture.rs for the original implementation)
        let full = fmt.alternate();
        let style = PrintFmt::Short;
        let cwd = std::env::current_dir();
        let mut print_path =
            move |fmt: &mut fmt::Formatter<'_>, path: backtrace::BytesOrWideString<'_>| {
                let path = path.into_path_buf();
                if !full {
                    if let Ok(cwd) = &cwd {
                        if let Ok(suffix) = path.strip_prefix(cwd) {
                            return fmt::Display::fmt(&suffix.display(), fmt);
                        }
                    }
                }
                fmt::Display::fmt(&path.display(), fmt)
            };
 
        let mut f = BacktraceFmt::new(fmt, style, &mut print_path);
        f.add_context()?;
        for frame in frames {
            f.frame().backtrace_frame(frame)?;
        }
        f.finish()?;
        Ok(())
    }
}

Wrapping it up

Here is the full implementation of the UnexpectedError with a Backtrace ready to use in your code:

use backtrace::{Backtrace, BacktraceFmt, BacktraceFrame, PrintFmt};
use std::fmt;
use std::fmt::Debug;
use thiserror::Error;
use tracing::error;
 
/// Generic Error
#[derive(Error, Debug)]
#[error("An unexpected error occurred: {message}")]
pub struct UnexpectedError {
    message: String,
}
 
impl UnexpectedError {
    pub fn from<T: std::fmt::Display>(err: T) -> Self {
        error!(
            error = %err,
            trace = ?Trace::new(),
            "Unexpected Error"
        );
        UnexpectedError {
            message: err.to_string(),
        }
    }
}
 
const FILTERS: [&str; 3] = [".cargo", "rustc"];
 
struct Trace(Backtrace);
 
impl Trace {
    pub fn new() -> Self {
        Self(backtrace::Backtrace::new())
    }
}
 
impl Debug for Trace {
    fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        // Filter out frames that are not relevant to the user
        let frames = self
            .0
            .frames()
            .iter()
            .filter(|frame| {
                let symbols = frame.symbols();
                for symbol in symbols {
                    if let Some(name) = symbol.filename() {
                        let filename = format!("{:?}", name);
                        if !FILTERS.iter().any(|filter| filename.contains(filter)) {
                            return true;
                        }
                    }
                }
                false
            })
            .collect::<Vec<&BacktraceFrame>>();
 
        // Print the backtrace (see capture.rs for the original implementation)
        let full = fmt.alternate();
        let style = PrintFmt::Short;
        let cwd = std::env::current_dir();
        let mut print_path =
            move |fmt: &mut fmt::Formatter<'_>, path: backtrace::BytesOrWideString<'_>| {
                let path = path.into_path_buf();
                if !full {
                    if let Ok(cwd) = &cwd {
                        if let Ok(suffix) = path.strip_prefix(cwd) {
                            return fmt::Display::fmt(&suffix.display(), fmt);
                        }
                    }
                }
                fmt::Display::fmt(&path.display(), fmt)
            };
 
        let mut f = BacktraceFmt::new(fmt, style, &mut print_path);
        f.add_context()?;
        for frame in frames {
            f.frame().backtrace_frame(frame)?;
        }
        f.finish()?;
        Ok(())
    }
}

Please let me know if this helped you!