There was a post and discussion on reddit yesterday that reminded me I wanted to write about stack traces in rust at some point.

I’ve worked on a bunch of java code bases over the years, and one feature I like about the java environment is that you can obtain a stack trace identifying the throw site of any exception raised and a some good information about the path the code took to get there.

This is really valuable in a production system where a subtle environment change triggers an unexpected violation of an invariant. The violation is unexpected and so the error bubbles all the way up to the generic error handling. Luckily because of the stacktrace you still have context from which to quickly diagnose the error and decide on an appropriate course of action.

Rust currently supports printing a stack trace to stderr when a thread panic!()s, which is really handy. It is enabled by setting the “RUST_BACKTRACE” environment variable. Panics however are too heavyweight to use for most errors in rust code because they offer limited recovery options, and instead it is generally preferred to return std::result::Result.

(Aside: I suspect in the long run panics will all-but disappear from rust. My guess is that some nice sugar will come along for Result types that will make e.g. array indexing with Results palatable (maybe something like ‘?’ syntax), and then panics will become relegated to a small corner of the language, used only for out-of-memory errors and maybe stack overflows.)

So that leaves a problem: How do I get a stacktrace out of an unexpected rust error in production code? The best approach I’ve come up with so far is to combine a custom error with rt::backtrace. Here’s the recipe:

  1. Create a custom error type
  2. #[derive(Debug,Clone)]
    pub struct MyError {
        pub msg: String,
        pub backtrace: String
    }
  3. Add a 'new()' fn. On creation insert a backtrace into the error using rt::backtrace
  4. impl MyError {
        pub fn new(msg: &str) -> MyError {
            MyError { msg: msg.to_string(), backtrace: get_backtrace() }
        }
    }
  5. For other error types used by the program, convert them using the convert::From machinery
    impl From<::std::io::Error> for MyError {
        fn from(e: ::std::io::Error) -> MyError {
            MyError::new(e.description().to_string())
        }
    }
  6. Create a Result type alias
  7. pub type Result<T> = ::std::result::Result<T, MyError>;

    Now step 3. ensures that when try!() converts an error from e.g. std::io::Error into MyError ...

    fn connect_to_something() -> Result<String> {
        let mut stream = try!(TcpStream::connect("example.com:1500"));   // try! creates MyError (and backtrace!)
        ...

    ... then the backtrace is created with this callsite. Unfortunately the backtrace won't point you to where the original std::io::Error was created, but at least you'll have an idea how connect_to_something() came to be called and the fact that the error originated in the TcpStream::connect. If you build the binary with dwarf debuginfo (rustc -g) then you'll even have a file and line number.

    The downside to this approach is that std::rt::backtrace::write() is unstable, so only works with nightly rust. Also it sounds like the backtrace functionality will be moved out to a separate crate at some point.