Confusing error message?

This is contrived incorrect code, which is similar to some code which I tried to write.

fn testing<'a>() -> Vec<&'a u32> {
    let a = 10;
    let mut b = Vec::new();
    let yy = || {
        b.push(&a);
    };
    b
}

According to my understanding, it is incorrect because b should not contain a reference to an already-dropped value. However, the compiler gives this error:

error[E0373]: closure may outlive the current function, but it borrows `a`, which is owned by the current function
 --> src/main.rs:4:14
  |
4 |     let yy = || {
  |              ^^ may outlive borrowed value `a`
5 |         b.push(&a);
  |                 - `a` is borrowed here
  |
note: closure is returned here
 --> src/main.rs:7:5
  |
7 |     b
  |     ^
help: to force the closure to take ownership of `a` (and any other referenced variables), use the `move` keyword
  |
4 |     let yy = move || {
  |              ++++

For more information about this error, try `rustc --explain E0373`.

Why is the closure returned at the end of the function?

Rust playground link: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=0a90cbcac1830afd836e623cdc29cc51

a is declared into the function, you push its reference to the vector b, then return the vector.

After the function returns, a is de-allocated and b contains a dangling reference to it.

That is exactly what I mean by "it is incorrect because b should not contain a reference to an already-dropped value.". However, the compiler's error message seems to say something different.

This function signature means that the Vec will only contain values that live at least as long as 'a, chosen by the caller. I.e., the Vec cannot contain any values that were not passed into it by the caller.

But a was not passed in, so trying to put it into the Vec is invalid. The a will be dropped, and the Vec will contain a dangling reference.

The error is confusing, probably because the closure is making it hard for the compiler to figure out what lifetime constraints are actually relevant here.


EDIT: Ok, so the above was totally wrong. This function should work, because 10 is a const, so it outlives 'a no matter what it is. The problem here is that the closure is borrowing a after it already captured it, which makes it into a short-lived lifetime.

If you had written this:

fn testing<'a>() -> Vec<&'a u32> {
    let a = &10; // <-- Note that we borrow here instead of inside the closure.
    let mut b = Vec::new();
    let yy = || {
        b.push(a);
    };
    b
}

... the function will compile fine, because the closure doesn't 'deallocate' a when it falls out of scope anymore.

So I think the real difference is probably that, for a fn item like testing, the &a value can be 'promoted' in a way that it can be assumed to be static, but for a closure, the capturing of a doesn't allow for a similar kind of promotion.

One thing I'll point out here is that that's a very suspicious signature. It's returning something borrowed, but there are no imputs from which to borrow stuff.

There are occasional uses for it, but you probably want to write -> Vec<&'static u32>. And by doing that you'd be more likely to get to something like

fn testing() -> Vec<&'static u32> {
    static a: u32 = 10;
    let mut b = Vec::new();
    let yy = || {
        b.push(&a);
    };
    b
}

which would work.

1 Like

I'm going to assume you understand the lifetime error and that this thread is about understanding the confusing error message. I agree that the compiler diagnostic could be better here. I searched for bugs about E0373 that looked like this, but didn't find any; you could make one.

(I think basically it's rare to create and only locally use a closure with this capturing lifetime issue, versus return it as part of an iterator chain, say, or pass it to another thread. Here's one bug with the "closure is returned" note where the closure is merely being used within the function, but that's not the target of the bug.)


The rest of this post is just a bunch of speculation about what the compiler is doing, for fun I guess. I'll start from here, with less abbreviated naming.

I think (from playing with it) that it's building an idea of the borrows that need to happen as it goes from top to bottom, but doesn't hit a conflict until the return statement, at which point it has to work backwards to try and come up with some human-friendly explanation of the conflict. In particular, if we tell the compiler what the lifetimes constraints of the vector must be up front:

-    let mut vector: Vec</*&'lt*/ _> = Vec::new();
+    let mut vector: Vec<&'lt _> = Vec::new();

Then you get a much better diagnostic that the borrow of local is longer than the lifetime of local itself, and nothing about the closure being "returned".

However in the original, when it sees this closure:

    let lambda = || {
        vector.push(&local);
    };

It doesn't know how long the borrow, and thus the capture, of local has to be. That depends on the type of vector, which it hasn't figured out yet. It just puts a placeholder in there -- "I have some Vec<T> -- well from what I'm pushing, it must be a Vec<&'? u32>. I need to capture local for '? to push into it. I don't know how long that is yet -- I'll come back to this when I know more later."

This unknown lifetime "infects" the lifetime of the closure. It's also limited by the lifetime of local. Otherwise, the closure looks fine.

Then when it sees vector is returned, this kicks in:

  • '?: 'lt (The lifetime of the references in vector must be at least as long as 'lt)

And this ties the returned lifetime, to the closure lifetime, to the capture of local. It's at this point, I'm guessing, it tries to fulfill the obligation of the capture of local first, and sees that it must be 'lt, and that this is longer than the local variable can live. When fulfilling this obligation, it has associated 'lt as the lifetime of the closure, knows that was deduced from the return expression, and ends up giving the inaccurate analysis about the closure being returned.

That's my speculation anyway. Note that the exact error also give some clues:

  • E0373 (the original error) is for "a captured variable in a closure may not live long enough", and the notes state that "it’s most commonly seen when attempting to return a closure"

And this commonality has apparently influenced the assumptions the diagnosis is making.


If you declare the local within the closure itself, you get an error about the lifetime of local within the closure instead. But the lifetime is still not tied to 'lt it seems -- I think it just realizes that the lifetime doesn't constrain the closure directly, and this is a problem. It's sort of like you wrote this:

    fn lambda(vector: &mut Vec<&u32>) {
        let local = 10;
        vector.push(&local);
    }

If you capture local (but not vector) with move, you get a more thorough diagnosis about the lifetime of the closure and of 'lt itself, even though the error is arguably the most general. Probably it deferred lifetime inference like before, but now the chain of lifetimes end with the lifetime of the closure due to the move. That is, there's no local capturing involved anymore due to the move, so it gets past that stage, but another borrow check kicks in with the error.


I definitely agree with this. Note for others: This is a crucial part of the change demonstrated:

-    let a = 10; // AKA `local`
+    static a: u32 = 10;

That is, only changing the lifetimes to return &'statics doesn't solve the problem (and the diagnostics are pretty much the same). Alternatively you can borrow from literals (like &10), and they'll be promoted to statics.

Borrowing from locals is the core problem.

9 Likes

I'm curious if this can be described as an &'a mut T<'a> problem in disguise?

  • Closure calls the method on &mut b and passes &a, so it captures a immutably and b mutably.
  • Since b is Vec<&'a u32>, we infer that a is borrowed by the closure for at least 'a (and then, if the closure was called, a would be lent further to the vector for exactly 'a). So, the closure itself lives for at least 'a.
  • Since b is Vec<&'a u32>, on the other side, it itself can live only as long as 'a, so closure can borrow b for at most 'a. So, the closure lives for at most 'a'.

No, it's just a &mut T<'a> problem. Consider @scottmcm's version; if it was a &mut 'a T<'a> problem, that would be a &'static mut T<'static> situation and fail. But it works fine, even if you call the closure a few times before returning. Same with fn testing(a: &u32) -> Vec<&u32> if you push the as via a closure.

The closure has to be valid for 'a to make sense, but there's nothing requiring the captured borrow of the vector to be that long.

(Also, in case pointing this out helps, &'x mut T is invariant in T but covariant in'x.)

1 Like

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.