Crate tres

source · []
Expand description

Effortless, low overhead error tracing in Rust.

Tres provides error traces with no stack unwinding and no symbol lookups. All you need to do is wrap your error type(s) with Traced<E> and make sure you use a Result type that supports return tracing.

Usage

Consider the following two “crates” with their respective error types:

mod crate_one {
    #[derive(Debug)]
    pub enum Error {
        FileTooLarge {
            size: u64
        },
        CrateTwo(crate_two::Error),
    }

    impl From<crate_two::Error> for Error {
        fn from(error: crate_two::Error) -> Self {
            Self::CrateTwo(error)
        }
    }

    pub fn foo() -> Result<(), Error> {
        let size = crate_two::file_size("foo.txt")?;
        if size > 1024 {
            return Err(Error::FileTooLarge { size });
        }
        Ok(())
    }
}

mod crate_two {
    #[derive(Debug)]
    pub struct Error(pub std::io::Error);

    impl From<std::io::Error> for Error {
        fn from(error: std::io::Error) -> Self {
            Self(error)
        }
    }

    pub fn file_size(filename: &str) -> Result<u64, Error> {
        let size = std::fs::File::open(filename)?
            .metadata()?
            .len();
        Ok(size)
    }
}

Converting the code to provide error traces can be done in a few simple steps:

  • Replace all Result<_, E> with Result<_, Traced<E>>
  • Use ErrorExt::traced() in all places where a new error is returned.

And that’s it! Any existing type conversions that were happening as a result of the ? operator will continue to work after switching over to tres.

Here’s what the final result looks like:

mod crate_one {
    use tres::{ErrorExt, Traced};

    /* ... */

    // Result uses `Traced`.
    pub fn foo() -> Result<(), Traced<Error>> {
        // `?` operator converts `Traced<crate_two::Error>` to `Traced<Error>`!
        let size = crate_two::file_size("foo.txt")?;
        if size > 1024 {
            // `ErrorExt::traced()` converts `Error` to `Traced<Error>`.
            return Err(Error::FileTooLarge { size }.traced());
        }
        Ok(())
    }
}

mod crate_two {
    use tres::Traced;

    /* ... */

    // Result uses `Traced`.
    pub fn file_size(filename: &str) -> Result<u64, Traced<Error>> {
        // `?` operator converts `std::io::Error` to `Traced<Error>`!
        let size = std::fs::File::open(filename)?
            .metadata()?
            .len();
        Ok(size)
    }
}

And if we run that code…

fn main() {
    let error = crate_one::foo().unwrap_err();
    println!("{:?}", error);
}

We get the output below, complete with an error trace!

CrateTwo(Error(Os { code: 2, kind: NotFound, message: "No such file or directory" })):
[src/lib.rs:51:20, src/lib.rs:26:20]

Caveat: Remember to propagate!

The error trace inside a Traced is appended to only when propagated using the try (?) operator. Because of this, it is important to ensure that all results in your code are propagated using the try operator, otherwise your error traces may end up missing certain locations.

This can be avoided by ensuring that all return values are surrounded by Ok(..?):

fn gives_error() -> Result<(), Traced<&'static str>> {
    Err(Traced::new("Oops!"))
}

fn foo() -> Result<(), Traced<&'static str>> {
    // !! NO !!
    gives_error()
}

fn bar() -> Result<(), Traced<&'static str>> {
    // !! YES !!
    Ok(gives_error()?)
}

TODO: implement a lint to detect missing Ok(..?).

Another option is to make use of try blocks. This makes it impossible to accidentally return a bare result without propagating it.

#![feature(try_blocks)]
fn foo() -> Result<(), Traced<&'static str>> {
    try {
        gives_error()?
    }
}
#![feature(try_blocks)]
fn bar() -> Result<(), Traced<&'static str>> {
    try {
        // Does not compile without `?` operator!
        gives_error()
    }
}

How it works

The error tracing behavior of tres is made possible by specializing the Result type’s behavior during try-propagation (? operator). When the Err variant of the result supports tracing (as indicated by the presence of the Trace trait), each invocation of the ? operator calls Trace::trace() on the error with the location of the ? invocation.

For now, this behavior must be provided by a third party Result type. The tres-result crate provides one; it is included in tres as Result.

Eventually, this crate is meant to be used with the standard library Result type. There will likely be an RFC to add a Traced trait to the standard library and specialize the behavior or core::result::Result in the same way that tres_result::Result is specialized.

Re-exports

pub use tres_result as result;
pub use result::Result::Err;
pub use result::Result::Ok;

Structs

A simple vector of Rust code locations.

Wraps a generic error value and keeps track of an error trace.

Enums

A drop-in replacement for core::result::Result that supports return tracing using the ? operator.

Traits

An extension trait applied to all untraced error types that allows conversion to Traced.

A trait for types that store an error trace.