Getting line numbers with `?` as I would with `unwrap()`

I have some websocket code than involves processing many Results that can fail for mostly network reasons. In my first version, I just used unwrap everywhere. When there's a network issue, I get a nice panic message including the file name and line number where it occurred.

However, I'd like to change all of the unwraps to ? and return a single Result from the task so that if there is a network failure, I can easily and robustly restart the task. The downside is that I lose the file name and line number where the error occurred. How can I get this information back?

For example, unwrap output:
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 13, kind: PermissionDenied, message: "Permission denied" }', src/main.rs:11:41
Output from printing the error returned using ?:

Os { code: 13, kind: PermissionDenied, message: "Permission denied" }
2 Likes

I'm not aware of any built-in way, but here's how I'd do it manually:

use std::error::Error;

// New error type encapsulating the original error and location data.
#[derive(Debug, Clone)]
struct LocatedError<E: Error + 'static> {
    inner: E,
    file: &'static str,
    line: u32,
    column: u32,
}

impl<E: Error + 'static> Error for LocatedError<E> {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        Some(&self.inner)
    }
}

impl<E: Error + 'static> std::fmt::Display for LocatedError<E> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}, {}:{}:{}", self.inner, self.file, self.line, self.column)
    }
}

// The core idea: convenience macro to create the structure
macro_rules! loc {
    () => {
        |e| LocatedError { inner: e, file: file!(), line: line!(), column: column!() }
    }
}

// And that's how it should be used:
fn main() {
    // Try opening the non-existent file
    let err = std::fs::File::open("dummy.txt")
        // In case of error - map it with the macro-generated mapper
        .map_err(loc!())
        // Now we know that this will be an error, in real case it can be propagated up
        .unwrap_err();
    // Print it to see how it works
    println!("{}", err);
}

Playground

1 Like

With the newly stabilized #[track_caller] feature (stable on Beta channel at the time of this writing, so should be part of the next Stable release), you can make it work together with the ? operator:

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

// New error type encapsulating the original error and location data.
#[derive(Debug, Clone)]
struct LocatedError<E: Error + 'static> {
    inner: E,
    location: &'static Location<'static>,
}

impl<E: Error + 'static> Error for LocatedError<E> {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        Some(&self.inner)
    }
}

impl<E: Error + 'static> std::fmt::Display for LocatedError<E> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}, {}", self.inner, self.location)
    }
}

impl From<std::io::Error> for LocatedError<std::io::Error> {
    // The magic happens here
    #[track_caller]
    fn from(err: std::io::Error) -> Self {
        LocatedError {
            inner: err,
            location: std::panic::Location::caller(),
        }
    }
}

fn main() -> Result<(), LocatedError<std::io::Error>> {
    std::fs::File::open("blurb.txt")?;
    Ok(())
}

I was actually thinking about using that for my own code base but was too lazy to investigate it until now, so thanks for motivating me to try it out! Might also be a nice feature for crates like thiserror.

9 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.