Detecting where an error occurred?


#1

Let’s say I have some software suite that performs a lot of floating point operations. Trigonometry or something. Say every single function returns a Result<float,err>, where err is a floating point error. Some functions call other functions in this suite, using try!() to easily return an error, if a sub-error occurs. And I myself use functions in that suite several times, in various places throughout my code. All my functions use try! as well, going all the way back to the main procedure, which can pattern match on some function like: fn perform_operations(…) -> Result<float,err>.

Let’s say perform_operations returns an error. How do I tell where that floating point error was created, and what part of my code, or of that software suite, has an error?


#2

Same way you’d do it in any other language without exceptions: reverse the program’s logic, lots of println!s, and instrumenting likely error generation sites.

I once had some code that would attach backtraces to Results, but ended up ditching it due to being hideously impractical and totally useless on Windows.


#3

The ? expression RFC has some discussion about this (the last 20 or so comments).


#4

Well, generally in any other language without exceptions, I’d do what you said, attach a backtrace to the result, or at least the program location and line. That’s typical in C debugging where you’ll #define ERROR(a) somenotifier(a,__FILE__,__LINE__); but if you say it’s impractical, I guess it’s just something you can’t do currently. Makes for more buggy code though, and harder to track down bugs, so I’d hope at least something could be done about it, especially in such a safety conscious language like rust.

Doesn’t seem that hard, honestly. You’d just have to have Result<Ok(T),Err(Error)> for an Error that’s specifically a tuple, that contained a message, a stack trace, a filename, a line number, etc… and whatever generated the Error could specify all that when the error occurred. Code that matched the error could get that information, and display it, and intermediary code could just use try! to pass it on up (since the backtrace has already been saved).

Lua does something like that with its xpcall macro (you can set an “error handler” to generate that information when it occurs, before it’s raised)


#5

All of that was what I meant by “instrumenting likely error generation sites”.

What’s impractical is attaching backtraces in general. If the error is an io::Error, for example… well, you can’t do anything with that. It’s defined by the stdlib, and you can’t just add a backtrace to it. So then you need to wrap non-backtrace-carrying errors in backtrace-carrying errors, but that’s playing whack-a-mole, so you try overriding try! to do it, but then you start having problems with coherence and orphan impls…

…at which point you scream “fuck it!” and just single-step through the program in gdb, which ends up being less effort than attempting to get a backtrace in anything vaguely like a general manner.


#6

Hm, I think that including file/line information about the deepest error should not be that difficult and should not cause much overhead, while providing a significant part of the benefits of stack traces.

There are macros for file and line. So you’ll need to define your error type, like

struct MyError {
    cause: Box<Error>,
    line: usize,
    file: &'static str,
}

And write your own macro and a trait which model try! and From, but with line numbers.

trait FromWithLocation<T> {
    fn from_with_location(t: T, file: &'static str, line: usize) -> Self
}

And


macro_rules! try_loc! {
    ( $e:expr) => { try!(e).map_error(|e| from_with_location(e, file!(), line!()))};
}

#7

It’s map_err, and you need to move it inside the try!.

A larger problem is that now you can’t inspect the error at all. You can respond to errors or have a backtrace, not both. Also, this only works provided every intermediate function uses try_loc!; returning a Result directly will skip that “frame” entirely, as will matching and transforming.

It is difficult because there is no way to do this that doesn’t devolve to “rewrite all your error handling code everywhere to introduce and/or preserve a manually constructed backtrace and/or wrap backtraces around types that don’t support them, but without making the errors useless”.


#8

It’s map_err, and you need to move it inside the try!.

Yeah, I’m terrible in writing code without compiling it :slight_smile:

I don’t try to solve bactrace problem in general here, I just want to have a location for the deepest error. And I think that this approach can be practical sometimes (though I haven’t tried it myself).

Oftentimes, you have an wrapper error type anyway, because you want to provide a single error type for upper layers or you want to provide some context (io errors lack information about filename which caused the error for example). So instead of your usual

enum MyError {
    IoError(io::Error),
    DatabaseError(database::Error), 
    ...
}
impl Form<io::Error)> for MyError{}
...

you’ll have

struct MyError {
    line: usize,
    file: &'static str,
    kind: MyErrorInner,
}

enum MyErrorInner {
    IoError(io::Error),
    DatabaseError(database::Error), 
    ...
}

impl FormWithLocation<io::Error)> for MyError { }
...

#9

Instead of a Result your functions could return a “signaling NaN”, and in a system language you should be able to raise an error when one of those signaling NaNs are generated.


#10

single-step through the program in gdb

wait… can you set a breakpoint in gdb at the construction of Err? That would solve all my issues at once.

…it’s probably some sort of elided inline non-op to create an Err, but it sure would be nice.


#11

at the very least you can set a breakpoint at f/i io::error::Error::new, but if some joker returns Err(string) I dunno what you could do besides single step through every single usage of their code… or do that wrapper thing, converting Err(string) to Err(JokerError) via JokerError::new(string).