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 wherespan!
is invoked. -
span!(1)
gives the span of the invocation of the macro which invokedspan!
-
span!(2)
gives the span of the invocation of the macro which invoked the macro which invokedspan!
. -
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!")
})
}
}