It is hoped that the community can further optimize the performance of track_caller in Result and Option

Some common methods on Result and Option do not have the track_caller tag, which makes it difficult to trace in user code.

For example:

    #[inline]
    #[stable(feature = "rust1", since = "1.0.0")]
    pub fn map_err<F, O: FnOnce(E) -> F>(self, op: O) -> Result<T, F> {
        match self {
            Ok(t) => Ok(t),
            Err(e) => Err(op(e)),
        }
    }

Can you elaborate? E.g. give an example use-case with an unhelpful error message and explain how #[track_caller] on map_err would have helped? Feel free to re-implement map_err to demonstrate adding it really does what you expect it does.

1 Like

map_err doesn't panic and the closure argument likely doesn't have #[track_caller] either. As such #[track_caller] on map_err will have no effect at all.

Maybe you mean you expect errors to have a backtrace themselves? #[track_caller] doesn't do that. It is not meant to support Result/Option.

There are error type wrappers like anyhow that support backtraces and easy wrapping in custom context to help track where the error came from.

I think @silence-coding wants things like this to work (or at least, I do):

use std::panic::Location;
use std::error::Error;

#[derive(Debug)]
struct TracedError {
    original: Box<dyn Error>,
    location: &'static Location<'static>,
}

#[track_caller]
fn capture(e: impl Error + 'static) -> TracedError {
    TracedError {
        original: Box::new(e),
        location: Location::caller(),
    }
}

fn failing() -> Result<(), std::fmt::Error> {
    Err(std::fmt::Error)
}

fn main() {
    dbg!(failing().map_err(capture));
}

Right now this captures the location of — FnOnce::call_once()?? Huh, worse than I expected.

Currently, it is possible to do this if you use ? rather than map_err (since there are then no higher-order-function call frames involved); this captures the location in main():

use std::panic::Location;
use std::error::Error;

#[derive(Debug)]
struct TracedError {
    original: Box<dyn Error>,
    location: &'static Location<'static>,
}

impl<E: Error + 'static> From<E> for TracedError {
    #[track_caller]
    fn from(e: E) -> TracedError {
        TracedError {
            original: Box::new(e),
            location: Location::caller(),
        }
    }
}

fn failing() -> Result<(), std::fmt::Error> {
    Err(std::fmt::Error)
}

fn captures() -> Result<(), TracedError> {
    Ok(failing()?)
}

fn main() {
    dbg!(captures());
}
[src/main.rs:29] captures() = Err(
    TracedError {
        original: Error,
        location: Location {
            file: "src/main.rs",
            line: 25,
            col: 8,
        },
    },
)

But if map_err() were track_caller, then that would be handy for situations where a From-conversion is not desirable.

1 Like

I'm working around it for that concrete use case by an "anchoring" wrapper which captures the location itself and forwards it to the eventual call ops:

use std::panic::Location;
use std::error::Error;

#[derive(Debug)]
struct TracedError {
    original: Box<dyn Error>,
    location: &'static Location<'static>,
}

#[track_caller]
fn anchor<E: Error + 'static>() -> impl FnOnce(E) -> TracedError {
    let location = Location::caller();
    move |e| TracedError {
        original: Box::new(e),
        location,
    }
}

fn failing() -> Result<(), std::fmt::Error> {
    Err(std::fmt::Error)
}

fn main() {
    dbg!(failing().map_err(anchor()));
}

That of course comes at an ergonomic cost in calling, reading and writing the code. Basically having to duplicate the constructors. It was always a minor annoyance but those tracing information is typically removed after prototyping anyways, so I never truly minded. That may be complacency though.

2 Likes