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.

2 Likes

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

Yes, I just want to implement this TracedError

A followup experiment: I tried to find out if this could be trivially patched into the standard library, and the answer is no.

  • I tried adding #[track_caller] to Result::map_err(), and, unsurprisingly given the previous experiment, this didn't help — the location reported was the declaration of FnOnce::call_once().
  • FnOnce::call_once() cannot have #[track_caller] since its ABI is not "Rust".

So, progress here would require the compiler to allow #[track_caller] on the "rust-call" ABI, which I would imagine is hard and possibly disruptive.

It's certainly a nontrivial amount of work, but FWIW, extern "rust-call" exclusively acts to make extern "rust-call" fn call(&self, args: (A, B, C)) have the same ABI as extern "rust" fn call(&self, a: A, b: B, c: C). Adding #[track_caller] to extern "rust-call" thus has a single obvious interpretation (to be equivalent to adding #[track_caller] to the splatted form).

That said, if you take the example and instead do failing().map_err(|e| capture(e)), the captured location is «crate::main::{{closure}} at ./src/main.rs:LL:CC» instead, which is much more useful.

Interestingly, with my test of

use std::panic::Location;

#[track_caller]
fn then_panic<T>(_: T) -> ! {
    dbg!(Location::caller());
    panic!()
}

fn main() {
    _ = Err::<(), _>(()).map_err(|e| then_panic(e));
}

the full panic backtrace shows something interesting: with the closure, the backtrace is Result::map_err/main::{{closure}}/then_panic, but without the closure, it's Result::map_err/FnOnce::call_once/then_panic. With then_panic as fn(_) -> ! we get a Location capture pointing at the fn keyword of fn then_panic and a backtrace of Result::map_err/FnOnce::call_once/then_panic{{reify.shim}}/then_panic

I think instead of making the FnOnce::call_once frame #[track_caller], I think that's the behavior to target if we want to improve behavior here; at least make it such that using the function object directly for a #[track_caller] acts like the closure rather than getting a FnOnce::call_once frame that doesn't show up otherwise.

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.