Confused by Rust once again

This

    use backoff::{retry, Error, ExponentialBackoff};
    fn test() -> Result<AuthResponse, String> {
        let op = || {
            reqwest::blocking::Client::new()
                .post("https://blah.com")
                .send()
                .map_err(Error::transient)
        };

        retry(ExponentialBackoff::default(), op)
            .map_err(|e| e.to_string())
            .and_then(|r| r.json::<AuthResponse>())
    }

results in (cargo check)

147 |     fn test() -> Result<AuthResponse, String> {
    |                                    ---------------------------- expected `Result<_, std::string::String>` because of return type
...
157 |             .and_then(|r| r.json::<AuthResponse>())
    |                           ^^^^^^^^^^^^^^^^^^^^^^^^ expected `Result<_, String>`, found `Result<AuthResponse, Error>`
    |
    = note: expected enum `Result<_, std::string::String>`
               found enum `Result<AuthResponse, reqwest::Error>`

The code transforms the error twice - in the closure, to backoff::Error, and then with map_err on the return value of retry, to String. How is it still a reqwest::Error at the point of that call?

These combinators don't seem to work as one would expect. This is very unintuitive.

Result::and_then only lets you transform the type of the Ok variant, not the Err variant. You gave it something with a String error type, but the closure in the and_then call returns something with a reqwest::Error type. That's what the compiler error is complaining about.

Sometimes the solution is to map_err last, but in this case, you have a Result<_, backoff::Error<_>> before the map_err (so it still doesn't match reqwest::Error). Instead you can map within the and_then.

retry(ExponentialBackoff::default(), op)
    .map_err(|e| e.to_string())
    .and_then(|r| r.json::<AuthResponse>().map_err(|e| e.to_string()))

(Untested.)

Or use something besides chaining if the nesting gets too ugly.

Or swap the order of map_err and and_then?

If I found the right retry, it doesn't return a request::Error, so that won't help. (That's what my second paragraph was talking about.)

((I'm not sure why and_then doesn't allow you to return a different error type incidentally...))
((Err, it has to be since the call is conditional. I was thinking of something else.))

1 Like

Of course I know and_then only transforms the result type, but map_err had already been called.

As I suspected, my confusion is due to the fact that these combinators don't create new values.

I don't understand what they're doing then.

So you can call map_err but it doesn't change the "error channel." That is confirmed by the fact that this compiles:

            let r: Result<reqwest::blocking::Response, Error<reqwest::Error>> =
                reqwest::blocking::Client::new()
                    .post("https://blah.com")
                    .send()
                    .map_err(backoff::Error::transient);

Definitely to my functional programming background, but this is very unintuitive indeed.

Looking at the definition of map_err

    pub fn map_err<F, O: FnOnce(E) -> F>(self, op: O) -> Result<T, F> {

it looks like it transforms the error channel to a new type as one would expect. Yet it does not.

I thought for a moment that the error channel of the result was lifted into another type by some compiler-invoked From, and that specifying types might fix it

        retry(ExponentialBackoff::default(), op)
            .map_err::<String, _>(|e| e.to_string())
            .and_then(|r| r.json::<AuthResponse>())

No - error is the same.

At this point I know that my confusion is due to my Java/Haskell/Scala background, but I still have no idea what's going on.

After the map_err call the error type should be String, not reqwest::Error. How is this happening? How does reqwest::Error stick around?

Can anyone with a Haskell/Scala background help me out?

Yes it does. What I think you've missed is that there are two possible reqwest::Errors entering your code. The second one is from Response::json() — you have to do a map_err on the Result that's coming from that call, not just the one from RequestBuilder::send().

When you write x().and_then(|...| y()), in order for that and_then to compile, you need x() and y() to have the same error type. Currently, your equivalent of x() has type Result<_, String>, and your equivalent of y() has type Result<_, reqwest::Error>.

4 Likes

Thank you.

I keep thinking Rust is doing something off or mysterious, which keeps me from following details to the extent required.

Lesson learned.

Your r.json() call returns Result<_, reqwest::Error>. The compiler says exactly that:

I.e., the map_err correctly transformed the first error into a String, but you've done nothing to the second error. Why should that be a String when clearly the function you are calling does not return a Result<_, String> but a Result<_, reqwest::Error>?

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.