rust-lang / rust-lang/rust

`AsyncFnOnce(…)` and `FnOnce<(…), Output : Future>` are not equivalent

Open
#139,173 4 comments 0 reactions 1 assignee View on GitHub

@compiler-errors is already working on this.

Since Mar 31, 2025.

A-async-closures C-discussion T-lang T-types
Dominant language
Rust
Stars
119k
Forks
16.1k
PR merge metrics
PR metrics pending

Description

The following two calls fail:

fn fut_closure<F: FnOnce() -> Fut, Fut : Future<Output = ()>>(f: F) {
    async_closure(f); // error, `F` is not `AsyncFnOnce`.
}

fn async_closure<F: AsyncFnOnce()>(f: F) {
    fut_closure(f); // error, `F` is not `FnOnce`.
}

even though future-outputting fns (e.g., fn … -> impl FutureOrSubtraitThereof), async fns, and async closure literals (async [move] |…| {…}) implement both flavors of the {Async,}FnOnce traits.

Why is this a problem

Rust 1.85.0 is too recent to be an acceptable MSRV for a bunch of projects, so the F: AsyncFnOnce version may not be usable in certain libraries.

This, in turn, entails that "middleware" functions either use the same matching bound as the library used (unless they perform eta-expansion of the closure to get back the double-impl that the compiler provides to async closures literals):

fn middleware1(f: impl AsyncFnOnce()) {
    ::lib::api(f); // Error
}

fn middleware2<Fut : Future<Output = ()>>(f: impl FnOnce() -> Fut) {
    ::lib::api(f); // OK, but now `api` can't switch to `AsyncFnOnce` without breaking us.
} // (and also, neither can we!)

And because they might do the former, it also means that in the future, once the MSRV gets to become high enough, the library author cannot replace the FnOnce bound with AsyncFnOnce without causing breakage[^turbofish].

[^turbofish]: there is also the question of explicit generic params vs. turbofish, but using a helper definition like in https://docs.rs/async-fn-traits does allow one to avoid exposing a turbofishable Fut in Rust 1.85.0, so this is not a true/genuine issue (just something to be mindful of).

Suggested Solution

[!CAUTION]
EDIT: this does not work because of the &Fn => FnOnce blanket impl: if we also had the suggested AsyncFnOnce => FnOnce blanket impl as well, then there would be an overlap for impl &Fn + AsyncFnOnce types.

  1. Add : FnOnce<Args, Output = Self::CallOnceFuture> as a super-trait of AsyncFnOnce,
  2. blanket-implement AsyncFnOnce for all impl FnOnce<Args, Output : Future> types,
  3. adjust the compiler-code to implement FnOnce only since AsyncFnOnce is now covered by the blanket impl.

This means that library authors can keep using FnOnce for lower-than-1.85.0 MSRV support, while hoping to migrate to the cleaner and nicer AsyncFnOnce eventually, e.g._, 1.88.0? 🤞), without requiring a major bump.

  • unless there are some compiler-specific implementation-related complications with doing this? cc @compiler-errors 🙏
Aside: this will not be possible for the AsyncFn{,Mut} traits

Since they're actually special in how they're actually Future-outputting LendingFn{,Mut}s rather than bare Future-outputting Fn{,Mut}s. But this fact should not affect FnOnce.

@rustbot label: +A-async-closures

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.