Skip to content

Point at the Fn() or FnMut() bound that coerced a closure, which caused a move error #144558

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

estebank
Copy link
Contributor

@estebank estebank commented Jul 28, 2025

When encountering a move error involving a closure because the captured value isn't Copy, and the obligation comes from a bound on a type parameter that requires Fn or FnMut, we point at it and explain that an FnOnce wouldn't cause the move error.

error[E0507]: cannot move out of `foo`, a captured variable in an `Fn` closure
  --> f111.rs:15:25
   |
14 | fn do_stuff(foo: Option<Foo>) {
   |             ---  ----------- move occurs because `foo` has type `Option<Foo>`, which does not implement the `Copy` trait
   |             |
   |             captured outer variable
15 |     require_fn_trait(|| async {
   |                      -- ^^^^^ `foo` is moved here
   |                      |
   |                      captured by this `Fn` closure
16 |         if foo.map_or(false, |f| f.foo()) {
   |            --- variable moved due to use in coroutine
   |
help: `Fn` and `FnMut` closures require captured values to be able to be consumed multiple times, but an `FnOnce` consume them only once
  --> f111.rs:12:53
   |
12 | fn require_fn_trait<F: Future<Output = ()>>(_: impl Fn() -> F) {}
   |                                                     ^^^^^^^^^ consider changing this bound to be `FnOnce`
help: consider cloning the value if the performance cost is acceptable
   |
16 |         if foo.clone().map_or(false, |f| f.foo()) {
   |               ++++++++

Fix #68119, by pointing at Fn and FnMut bounds involved in move errors.

…caused a move error

When encountering a move error involving a closure because the captured value isn't `Copy`, and the obligation comes from a bound on a type parameter that requires `Fn` or `FnMut`, we point at it and explain that an `FnOnce` wouldn't cause the move error.

```
error[E0507]: cannot move out of `foo`, a captured variable in an `Fn` closure
  --> f111.rs:15:25
   |
14 | fn do_stuff(foo: Option<Foo>) {
   |             ---  ----------- move occurs because `foo` has type `Option<Foo>`, which does not implement the `Copy` trait
   |             |
   |             captured outer variable
15 |     require_fn_trait(|| async {
   |                      -- ^^^^^ `foo` is moved here
   |                      |
   |                      captured by this `Fn` closure
16 |         if foo.map_or(false, |f| f.foo()) {
   |            --- variable moved due to use in coroutine
   |
help: `Fn` and `FnMut` closures require captured values to be able to be consumed multiple times, but an `FnOnce` consume them only once
  --> f111.rs:12:53
   |
12 | fn require_fn_trait<F: Future<Output = ()>>(_: impl Fn() -> F) {}
   |                                                     ^^^^^^^^^
help: consider cloning the value if the performance cost is acceptable
   |
16 |         if foo.clone().map_or(false, |f| f.foo()) {
   |               ++++++++
```
@rustbot
Copy link
Collaborator

rustbot commented Jul 28, 2025

r? @lcnr

rustbot has assigned @lcnr.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Jul 28, 2025
@rustbot
Copy link
Collaborator

rustbot commented Jul 28, 2025

This PR modifies tests/ui/issues/. If this PR is adding new tests to tests/ui/issues/,
please refrain from doing so, and instead add it to more descriptive subdirectories.

@rust-log-analyzer

This comment was marked as resolved.

@rust-log-analyzer

This comment has been minimized.

Copy link
Contributor

@lcnr lcnr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally don't think this change is worth it 🤔

I think that most functions which require an Fn/FnMut do so as they intend to call it multiple times, so suggesting to weaken this bound is likely to be incorrect.

While I think adding a note which explains that FnMut closures may be called multiple times and therefore must not consume their captured values could be useful to some users, I worry that these error messages are already very large: playground

error[E0507]: cannot move out of `foo`, a captured variable in an `Fn` closure
  --> src/lib.rs:11:25
   |
10 | fn do_stuff(foo: Option<Foo>) {
   |             ---  ----------- move occurs because `foo` has type `Option<Foo>`, which does not implement the `Copy` trait
   |             |
   |             captured outer variable
11 |     require_fn_trait(|| async {
   |                      -- ^^^^^ `foo` is moved here
   |                      |
   |                      captured by this `Fn` closure
12 |         if foo.map_or(false, |f| f.foo()) {
   |            --- variable moved due to use in coroutine
   |
note: if `Foo` implemented `Clone`, you could clone the value
  --> src/lib.rs:1:1
   |
1  | struct Foo;
   | ^^^^^^^^^^ consider implementing `Clone` for this type
...
12 |         if foo.map_or(false, |f| f.foo()) {
   |            --- you could clone this value

I think the main issue here is that we're pointing to Foo not being Clone instead of the fact that map_or moves the Option. We should extend our "use .as_ref()" suggestion to extend to this snippet but not add any additional information about the way Fn/FnMut closures work as while potentially useful, I think it's not worth the noise for most users (and stops being useful after seeing it for the first few times)

@estebank
Copy link
Contributor Author

I think that most functions which require an Fn/FnMut do so as they intend to call it multiple times, so suggesting to weaken this bound is likely to be incorrect.

While I think adding a note which explains that FnMut closures may be called multiple times and therefore must not consume their captured values could be useful to some users, I worry that these error messages are already very large

I agree that the diagnostic for these cases is getting a bit long, my rule of thumb is that the more uncommon a diagnostic is the less problematic it is to be more communicative. For this case in particular there are 3 possibilities that a person would follow: 1) clone the value (what we already suggest), 2) use .as_ref() (which only works when Option or Result are involved) or 3) changing the closure needed. I feel that when encountering these situations, the best thing we can do for our users is give them as much context as possible. For these cases we can't know a priori what the right solution would be. Looking through the affected tests tests/ui/borrowck/unboxed-closures-move-upvar-from-non-once-ref-closure.stderr is the closest one to be aided by 2), and even then it is not a case of calling as_ref() but rather calling .iter() instead of .into_iter().

I think the main issue here is that we're pointing to Foo not being Clone instead of the fact that map_or moves the Option. We should extend our "use .as_ref()" suggestion to extend to this snippet but not add any additional information about the way Fn/FnMut closures work as while potentially useful, I think it's not worth the noise for most users (and stops being useful after seeing it for the first few times)

I think that cloning is the most natural thing someone would try, getting ahead of them and letting them know that the type isn't Clone has value, imo. We should provide the context of why a closure became of a given type. Specially for FnMut. It might be that a closure being required to be FnMut is strong signal that the closure isn't intended to be FnOnce, but I don't think that an Fn closure gives us the same strong signal, it is just the "default" people would lean towards.

I'm just concerned with not showing information that is needed in a subset of cases because it is always adding to the verbosity. Specially for ownership errors, which can be notoriously tricky to understand when not given enough information. For errors that happen more often (E0277, E0308), I'd be more concerned with optimizing for terse output.

@estebank
Copy link
Contributor Author

CC #47850, which is the same case handled here for function calls but for methods.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Error message for closure with async block needs improvement
4 participants