Forwarding errors from a callback when you have errors of your own


#1

Suppose I provide a utility function which takes a callback that can fail:

(I know what you’re thinking about the FnMut trait object. Shush.)

fn bisect<E>(
    (lo, hi): (f64, f64),
    compute: &mut FnMut(f64) -> Result<f64, E>,
) -> Result<f64, E>

However, there are ways in which my own function can fail. So I suppose I need my own wrapper error:

struct Error<E> {
    // one could perhaps argue this case is a contract
    // violation and panic-worthy, but I'm just trying to
    // include more than one error in the example
    BadBounds,
    NoMinimum,
    ComputeError(E),
}

fn bisect<E>(
    (lo, hi): (f64, f64),
    compute: &mut FnMut(f64) -> Result<f64, E>,
) -> Result<f64, Error<E>>

But it doesn’t appear to me that, for instance, error chain supports generic errors. So then I have to write a bunch of boilerplate and decide on arbitrary-feeling bounds like Error + Send + 'static or whatnot. Also, surely the calling code cares a lot more about its own errors than those produced by this library, so erasing the type of E feels like a non-option. Even putting my own errors in that same channel feels invasive.

How do others handle this sort of situation in a manner that places reasonable expectations on the user?


#2

How about stacking the results like Result<Result<f64, E>, YourError>?


#3

I’ve done this a couple of times and felt bothered about deciding which way to order the errors.

Coming from the Haskell mindset of monads, I have a natural tendency to think E belongs on the outer Result. We take a function that short-circuits to E and produce a function that does the same. In haskell, it’s what you would end up if you started out with a bisect function that took an a -> b callback and then created a monadic bisectM variant whose callback is a -> m b.

But then if I had to consider which case deserves the label of a “successful failure”, it would be E, not my error.

That said, it does compose the most easily and with the least boilerplate.


#4

I think of it as follows: user function returns Result<f64, E> - without your function’s involvement that’s what a caller handles. But, your call occurs first and mostly does error checking - as such, it’s only real return type of its own is YourError. The Result<Result<...>, YourError> seems to convey that nicely.


#5

To be clear, the user’s function in this case gets called multiple times, and bisect short-circuits as soon as it encounters an E. NoMinimum is an error case that can only be determined after many successful calls to the user function.

That said, I do suspect that my reasoning for putting E on the outside mostly stems from me trying to bring in the paradigms of another language.


#6

Right, makes sense. I still think the separation of the results is best - your code is the driver of the caller’s code, doesn’t provide the “true” Ok result itself (ie the f64 here), and they fail in completely different manners; your failures are more to do with failing to drive the caller to successful completion, whereas their failures are something else entirely (opaque).

But I can see an argument for blending them too. I’d go with the separation myself, I think.