The dreaded "returns a value referencing data owned by the current function"

Consider:

    let response = retry(backoff(), || {
        match http_client
            .get(url.as_str())
            .send()
            .map_err(backoff::Error::transient)
        {
            Ok(response) => match response.status() {
                StatusCode::OK => Ok(response),
                StatusCode::NOT_FOUND => Err(backoff::Error::transient("not ready...")),
                _ => {
                    let error = format!("non-200 status code: {}", response.status()).clone();
----------->        Err(backoff::Error::permanent(error.as_str()))
                }
            },
            Err(e) => Err(backoff::Error::permanent("Fatal error:")),
        }
    });

The line with the arrow generates "returns a value referencing data owned by the current function" no matter what I try. With the above variant (I've tried cloning, etc.), it's:

115 |                     Err(backoff::Error::permanent(error.as_str()))
    |                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^-----^^^^^^^^^^^
    |                     |                             |
    |                     |                             `error` is borrowed here
    |                     returns a value referencing data owned by the current function

(I honestly cannot believe this is so difficult at this stage. I've written a few thousands lines of Rust now.)

I do see that I'm taking a reference to stack memory - error. But what else can I do?

Trying to figure out what's going on here, I see that backoff's Error enum is type-parameterized and that permanent is defined like this:

pub fn permanent(err: E) -> Self

My first question is why is the type param inferred as &str instead of, say, String? The latter would not result in this error.

Can I force it to type as Error?

Can someone point out the clean, idiomatic way to handle something like this?

Ok, I just fixed it with explicit typing:

    let response: Result<reqwest::blocking::Response, backoff::Error<String>> =
        retry(backoff(), || {
            match http_client
                .get(url.as_str())
                .bearer_auth(token.access_token().secret())
                .header("api-version", "2")
                .send()
                .map_err(backoff::Error::transient)
            {
                Ok(response) => match response.status() {
                    StatusCode::OK => Ok(response),
                    StatusCode::NOT_FOUND => Err(backoff::Error::transient(
                        "Response not ready...".to_string(),
                    )),
                    _ => {
                        let error = format!("non-200 status code: {}", response.status()).clone();
                        Err(backoff::Error::permanent(error))
                    }
                },
                Err(e) => Err(backoff::Error::permanent("Fatal error:".to_string())),
            }
        });

I still say this shouldn't be this hard. :slight_smile:

You might be able to use Cow<'static, str> instead (if you care enough).

You solved your own question too quick for me to be completely sure, but trying to use a borrow there probably would have introduced borrowing problems even if closures could return borrows of their own fields.

1 Like

Because you're passing it a &str in all three call sites:

Err(backoff::Error::transient("not ready..."))
Err(backoff::Error::permanent(error.as_str()))
Err(backoff::Error::permanent("Fatal error:"))

Calling to_string() or similar on the string literals and dropping the .as_str() call would fix things without the need for explicit type annotations.

Inference is the compiler's way of describing your code back to you. It's looking at what you wrote and making conclusions about whether the types you've used are or are not consistent.

The type of

Err(backoff::Error::permanent("Fatal error:"))

can only be Result<T, backoff::Error<&'a str>> for some OK type T and some suitable lifetime &a. There is no other type that accurately describes what you wrote. If you want the compiler to use some other type, you need to write code that produces that type. For example, the type of

Err(backoff::Error::permanent("Fatal error:".to_string()))

can only be Result<T, backoff::Error<String>> for some OK type T, as you want. You would then also need to update the other arms of your nested match statements to ensure that they also evaluate to the same type: type inference can't fix inconsistencies in your code, only tell you that they exist.

1 Like

It's not that hard if you actually try to understand what you are doing.

Yes, but that code still looks as if it's written in the “let me randomly add .clone, .as_ref, .as_str and other such “useless calls” till compiler would finally shut up”. One example:

let error = format!("non-200 status code: {}", response.status()).clone();

What does that .clone achieves? Nothing, of course, because format! already created a brand-new owned string there for you.

Most likely it was added when compiler suggested it, without thinking – but compiler is not magic, it's not supposed to be used as a substitute for understanding.

Compiler is brainless and insane by definition (we know how it works and there are literally no place where “brain” or “sanity” may exist) and if no one else understands what the code does, too… of course it would be hard to make it work!

There are some corner-cases where compiler rejects code that could have worked, but here you are just trying to do something that would never work: return reference for the memory that's allocated locally… compiler complains correctly, but it couldn't untangle your code, only you can do that.

Yes I figured this out which I why I fixed it with explicit typing.

(The inference engine must have (naturally) used the first instance of Error in the block to set the type.)

P.S. Yes, I regret the "too hard" comment. The type inference going on with backoff::Error was something I hadn't come across before. At first I was looking at this as a typical use of format!.

3 Likes