[Pre-RFC] Backtraces for error handling


#1

Abstract

Something that wearies me is that computer software - including but not limited to Rust software - often fails to give useful, detailed diagnostics. This Pre-RFC proposes some changes to better support rendering backtraces and to encourage people to print these backtraces.

Description

Add a Span type to the standard library which represents spans of source code, similar to the type used in the compiler.

impl Span {
    fn filename(&self) -> &str { ... }
    fn line(&self) -> u32 { ... }
    fn column(&self) -> u32 { ... }
}

When a program is built in debug-mode, its spans can be used to render the chunk of code they represent (in its surrounding context), similar to rustc’s diagnostics output. When built in release, the relevant piece of source is not included in the binary.

Spans can be created either through a built-in macro or through the ? operator. The span!(n) macro takes a compile-time-constant n: u32 such that:

  • span!(0) gives the span of where span! is invoked.
  • span!(1) gives the span of the invocation of the macro which invoked span!
  • span!(2) gives the span of the invocation of the macro which invoked the macro which invoked span!.
  • span!(3) etc.

Extend the Carrier trait (or whatever it’s called now) to take an extra Option<Span> argument when doing an error conversion.

trait Carrier {
    fn from_error(err: Self::Error, span: Option<Span>) -> Self;
}

When the compiler invokes this method in debug mode, is passes Some(span), giving the span of code which created the error, and ensures that the relevant lines of code are included in the compiled binary for rendering. In release mode, None is passed instead.

Add a FromSpanned trait which is similar to From but also takes an Option<Span>. There is a blanket impl of FromSpanned<E> for anything which impls From<E>.

trait FromSpanned<E> {
    fn from_spanned(err: E, span: Option<Span>) -> Self;
}

impl<T, E> FromSpanned<E> for T
        where T: From<E>
{
    fn from_spanned(err: E, _span: Option<Span>) -> T {
        From::from(e)
    }
}

Make the Carrier impl for Result use FromSpanned rather than From.

Extend the Error trait with an extra method to get the error’s span.

trait Error {
    fn span(&self) -> Option<Span> {
        None
    }
}

Lastly, extend fmt::Formatter with a method to render a spanned error.

impl fmt::Formatter {
    fn fmt_error(&mut self, error: &Error, f: F) -> fmt::Result
        where F: FnOnce(&mut fmt::Formatter) -> fmt::Result
    {
        ...
    }
}

The exact behaviour of this method depends on whether the alternate flag (#) was set on the Formatter. If it is, it prints an error message, a span (if available), and the error’s cause (if there is one), in a manner resembling the compiler’s output.

error: this text is written by the callback to fmt_error
  --> src/main.rs:28:1
   |
28 |  lets_make_an_error(
   |  _^ starting here...
29 | |     
30 | | )?;
   | |_^ ...ending here
   |
caused by: This text is written by the Display impl of the error's cause

Because spans are only included in debug builds of an app, only developers will see the chunk of source code. Regular users will simply see an error with a list of causes. Without the alternate flag, this method may print something more like error: this text is written by the callback to fmt_error: this text is written by the Display impl of the error's cause.

The point of this method to unify the way unrecoverable errors are presented to the user. Currently, people tend to either use Error::description or Display::fmt to print errors. Error::description is problematic because it can’t return an allocated string and so can’t display the cause of an error. Display::fmt should be encouraged but it suffers from the problem that many implementors don’t bother or think to display the cause of the error and the full trace is lost. Instead, we should encourage users to start implementing Display::fmt for errors by just calling fmt_error, eg.

impl fmt::Display for MyError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.fmt_error(self, |f| {
            write!(f, "i'm an error yo!")
        })
    }
}