Async Closures

This post attempts to motivate some of the concrete technical reasons why the #![feature(async_closure)] that I've been working on recently[1][2][3][4] is so complicated. Specifically, why can't we just use || async {} that we can express on stable today, an interesting but very important note about the relationship between a "lending" FnMut and the FnOnce trait that I haven't seen written anywhere yet, and how async closures are a tractable solution to an otherwise intractable problem with lending closures.

We have async closures at home

You may have tried in the past to make your closures async by writing a regular closure that returns an async block:

let x = || async {};

This is indeed a closure that returns a future. However, let's try actually using it practically.

Imagine having some service framework, and a query that you can use to do something for each of its registered services:

/// Queries a database for all services, allowing you
//  to run a callback for each service definition.
async fn query_all_services(
    x: impl FnMut(ServiceDefinition) -> impl Future<Output = ()>,
) { /* impl */ }

side-note that's not valid Rust[5]. So, let's try that again:

async fn query_all_services<F, Fut>(x: F)
where
    F: FnMut(ServiceDefinition) -> Fut,
    Fut: Future<Output = ()>,
{ /* impl */ }

And now let's try to use this pseudo-async closure.

For example, let's gather up all of the service IDs for every service, so we can list them. Imagine we have some async fn ServiceDefinition::id(&self) -> ServiceId accessor[6], and call it in our closure:

let mut service_ids = vec![];
query_all_services(|svc| async {
    service_ids.push(svc.id().await);
});

Uh

error: captured variable cannot escape `FnMut` closure body
  --> src/main.rs:22:30
   |
21 |       let mut service_ids = vec![];
   |           --------------- variable defined here
22 |       query_all_services(|svc| async {
   |  ____________________________-_^
   | |                            |
   | |                            inferred to be a `FnMut` closure
23 | |         service_ids.push(svc.id().await);
   | |         ----------- variable captured here
24 | |     });
   | |_____^ returns an `async` block that contains a reference to a captured variable, which then escapes the closure body
   |
   = note: `FnMut` closures only have access to their captured variables while they are executing...
   = note: ...therefore, they cannot allow references to captured variables to escape

Let's read that error message carefully:

returns an async block that contains a reference to a captured variable, which then escapes the closure body

Why are we returning a reference to a captured variable? Oh, yeah we capture a mutable reference to service_ids in order to push the IDs onto it.

let mut service_ids = vec![];
query_all_services(|svc| async {
    service_ids.push(svc.id().await);
//  ^^^^^^^^^^^ We capture `&mut Vec<ServiceId>` here.
});

In order call the callback multiple times, the closure reborrows the mutable reference it captured on every invocation, and it hands back a future which borrows that mutable reference from the closure itself.

So what gives? Is this really something we can't fix?

Async closures are (kinda) lending closures

This limitation is related to an interesting blog post that Niko wrote a while back: "Giving, lending, and async closures"[7].

Specifically, FnMut is not currently a lending trait, since its output type does not have a lifetime that allows us to link the &mut self of the call with the output.

But what if it were? Let's try to adapt the LendingFnMut trait that Niko laid out.

But first, let's do a bit of due diligence keep in mind the trait hierarchy that we are accustomed to in Rust. Specifically, FnMut is a subtrait of FnOnce, because if we can call a closure multiple times we should definitely be able to call it once. So adapting that to our LendingFnMut trait:

trait LendingFnMut<A>: FnOnce<A> { // `FnOnce` supertrait.
    fn call_mut<'s>(&mut self, args: A) -> Self::Output<'s>;
}

trait FnOnce<A> {
    type Output<'this>
    where
        Self: 'this;
    
    fn call_once<'s>(self, args: A) -> Self::Output<'s>;
}

Well that's odd what's up with that 's lifetime on call_once future? It's not present anywhere in the inputs it's just kinda dangling there in the output.

Is there anything else we could put there instead? If we put 'static there, then we'd have a really restrictive type Output, since we would then need to satisfy Self: 'static How strange. Well, let's keep it for now. It definitely won't come back to bite us later

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Ok, let's try these definitions out. First, let's express our query_all_services function with LendingFnMut:

async fn query_all_services<F, Fut>(x: F)
where
    F: for<'s> LendingFnMut<ServiceDefinition, Output<'s>: Future<Output = ()>>,
{ /* impl */ }

OK, kind of a hairy definition, but nothing we can't take apart piece-by-piece.

So what it's saying is that F is a lending closure whose output type is a Future that returns () when awaited. It's using the associated type bound syntax that I recently stabilized, and it's saying that the output is some Future type that awaits to (), which may be generic over the 's parameter that's the lending part.

So yeah, now let's write the caller code. Annoyingly we need to write a manual implementation of this trait instead of using a closure let's try doing that, using the hopefully-soon-to-be-stabilized #[feature(impl_trait_in_assoc_type)]:

let mut service_ids = vec![];
query_all_services(Callback { service_ids: &mut service_ids });

struct Callback<'a> {
    service_ids: &'a mut Vec<ServiceId>,
}

impl<'a> FnOnce<ServiceDefinition> for Callback<'a> {
    type Output<'this> = impl Future<Output = ()>
    where
        Self: 'this;
    
    fn call_once<'s>(self, svc: ServiceDefinition) -> Self::Output<'s> {
        async move {
            self.service_ids.push(svc.id().await);
        }
    }
}

Cool, we've got the FnOnce implementation down. What about the LendingFnMutOnce implementation

impl<'a> LendingFnMut<ServiceDefinition> for Callback<'a> {
    fn call_mut(&mut self, svc: ServiceDefinition) -> Self::Output<'_> {
        async move {
            self.service_ids.push(svc.id().await);
        }
    }
}
error[E0308]: mismatched types
  --> src/lib.rs:46:9
   |
33 |       type Output<'this> = impl Future<Output = ()>
   |                            ------------------------ the expected future
...
45 |       fn call_mut(&mut self, svc: ServiceDefinition) -> Self::Output<'_> {
   |                                                         ---------------- expected `<Callback<'a> as FnOnce<ServiceDefinition>>::Output<'_>` because of return type
46 | /         async move {
47 | |             self.service_ids.push(svc.id().await);
48 | |         }
   | |_________^ expected future, found `async` block
   |
   = note: expected opaque type `<Callback<'a> as FnOnce<ServiceDefinition>>::Output<'_>`
            found `async` block `{async block@src/lib.rs:46:9: 48:10}`

Oh, that's awkward. Every async closure is indeed a separate type what do we do? Let's try to reuse one for the other by defining the FnMut implementation first, and then dispatching the FnOnce implementation to that, like regular closures do[8]:

[code omitted because it's a disaster]

Well that also doesn't work. I won't go into major details, but here's the code if you're curious. I'll summarize the gist of the issue in just a second. First, let's think about this more abstractly.

Two different output types

So, it turns out that we made a mistake when laying out the definition of the LendingFnMut/FnOnce trait hierarchy. Specifically, we need two different output types:

trait LendingFnMut<A>: FnOnce<A> {
    // Let's call the lending flavor of this output `LendingOutput`.
    type LendingOutput<'this>
    where
        Self: 'this;
    
    fn call_mut<'s>(&'s mut self, args: A) -> Self::LendingOutput<'s>;
}

trait FnOnce<A> {
    // Whereas the output of `FnOnce` is -- this makes sense, there's
    // really no borrowing happening here, so it makes sense that this
    // output doesn't need to be parameterized over a lifetime.
    type Output;
    
    fn call_once(self, args: A) -> Self::Output;
    // That solves out mysterious lifetime question from earlier, too.
}

OK and if we try to employ this:

impl<'a> FnOnce<ServiceDefinition> for Callback<'a> {
    type Output = impl Future<Output = ()>;

    fn call_once(self, svc: ServiceDefinition) -> Self::Output {
        async move {
            self.service_ids.push(svc.id().await);
        }
    }
}

impl<'a> LendingFnMut<ServiceDefinition> for Callback<'a> {
    type LendingOutput<'s> = impl Future<Output = ()>
    where
        Self: 's;

    fn call_mut(&mut self, svc: ServiceDefinition) -> Self::LendingOutput<'_> {
        async move {
            self.service_ids.push(svc.id().await);
        }
    }
}

Hey! It works! Check for yourself: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=358bad2b87783e690163886f329c7e37

So what's going on here? We literally just copied the body from call_once to call_mut, right?

Wait let's read between the lines in the implementations of both functions. While both of them share the same set of syntax tokens, each actually ends up capturing a different type! Specifically, the call_once future ends up capturing Self (i.e. Callback<'a>), and the call_mut future ends up capturing &mut Self (i.e. &mut Callback<'a>).

That's interesting, and actually the root cause of the issue in the code I linked above. Specifically, there's no way to share the implementation between LendingFnMut and FnOnce because generally return different futures!

OK, so stepping back out of the world of manual trait implementations, for this to work for real closures, we just need to teach rustc how to generate this new definition of LendingFnMut, with its own distinct return type, whenever it gets into a situation where it detects a borrow coming from the closure itself, right? Will that work in general?

Lending closures are not typically FnOnce

Let's take a second to step and think about a general lending closure. For example, imagine a closure that lends out a reference to a string that it captures by value:

let string = String::from("hello, world!");
let view_string: /* impl Fn() -> &str */ = move || &string;

Now, since all closures implement FnOnce, what would the FnOnce implementation for that closure even return? Reminder that the FnOnce signature is:

fn call_once(self, args: Args) -> Self::Output;

Notably, we consume the self argument in the process of calling the closure. So if we've moved the String into the closure, we consequently drop it.

It turns out that this is intimately related to the problem we hit above, where we have separate bodies for the LendingFnMut and FnOnce implementation. Unlike the async case above, we can't just copy the body:

impl FnOnce<()> for /* {the `view_string` closure} */ {
    type Output = /* what do we even put here!? */;
    
    fn call_once(self) -> Self::Output {
        &self.string
        // Also, oh no! We have an escaping reference.
    }
}

So what do we do? Well, I guess we could specify a totally separate body for the FnOnce impl for the lending closure for example, we could make it so that the closure returns the captured string by value when it's called with FnOnce::call_once

but, I don't believe this process generalizes in a useful way. I could be the devil's advocate, and ask "what if"s about so many different types that would require their own strategy to fix. For example, you could imagine lending closures that return arbitrarily complex types, like Ref<'_, T>, repr(transparent)-transmuted references, etc.

This is a very important detail that I don't think was discussed in Niko's lending blog post (maybe it was, but I didn't see it

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
) however, it is a very important detail since it affects the way we design async closures.

All async closures admit a FnOnce implementation

So given that problem above, what makes async closures doubly interesting is that it really doesn't suffer it at all, actually.

Specifically, I'm claiming that they really do admit an obvious FnOnce implementation even if you only ever provide the LendingFnMut implementation. We can always create a meaningful FnOnce implementation by taking the Future returned by the async closure and making all of the types that were captured by-ref into ones captured by-move, and adjusting some borrows to make up for that.

That's basically what we did when we provided the FnOnce implementation above it just came for free to us because we captured self in the resulting Future rather than &mut self. In reality, it's a bit more complicated when you factor in the move keyword, the 2021-closure-capture rules laid out in RFC 2229.

I was going to go into more detail exactly how this transformation worked, since there are a lot of fun caveats having to do with captures modes and mutability and the FnOnce trait and stuff, but I didn't have enough space to write in the margins (/jk) and in reality I'm just a bit too lazy to write it all up, since it's really technical. If you want to see the nasty gory details, check out the by_move_body pass that I implemented, which transforms the body of a by-ref future into a by-move one. This is what takes the returned future of LendingFnMut[9] and turns it into a future that is compatible with FnOnce: by_move_body.rs in the compiler. You may also want to read here and here in the upvar.rs capture analysis code in the type checker.

An AsyncFnOnce hierarchy:

Somewhat unrelated to the problems above (but useful for some summarizing discussion below), it's actually most useful to abandon LendingFn* for now and define a set of AsyncFn* traits that are specialized to async closures. I proposed this in PR #119305. Specifically, we have:

trait AsyncFnOnce<A> {
    type CallOnceFuture: Future<Output = Self::Output>;
    
    /// Value returned from awaiting the future.
    type Output;
  
    fn call_once(self, arg: A) -> Self::CallOnceFuture;
}

trait AsyncFnMut<A> {
    type CallMutFuture<'s>: Future<Output = Self::Output>
    where
        Self: 's;
    // Notably, this is a different future type, but `Self::Output`
    // is the same.
    
    fn call_mut(&mut self, arg: A) -> Self::CallMutFuture<'_>;
}

/// `AsyncFn` left out since it's practically the same as `AsyncFnMut`.

These traits will be perma-unstable just like the Fn traits, and they will only be nameable through the "paren sugar" trait bounds that users are used to.

Specifically, users can just write F: AsyncFnMut() -> i32. We will likely simplify this with an async trait bound modifier rather than naming AsyncFnMut directly (e.g. F: async FnMut() -> i32), but we'll keep the trait perma-unstable to name directly. This means that we may swap out this trait for a general LendingFnMut trait (and make AsyncFnMut into a trait alias of sorts).

This set of traits also simplifies some other problems that come with a general LendingFnMut trait, that I don't have time to go into in this post, specifically:

  • It makes async closure signature inference possible.
  • It avoids some higher-ranked trait bound issues that would be present with LendingFnMut.
  • It allows us to tailor error messages for when this trait is unimplemented.

Final thoughts

Why does this work for async closures, but LendingFnMut doesn't have an intuitive solution in general?

Well, async closures can be seen as a specialization of lending closures, specifically limiting the return type in a way that makes tractable to problem of how to generalize closure's body into a meaningful FnOnce implementation.

Specifically, while the user has the power to specify whatever Output type that they want, they still can't write futures whose output captures from the closure, that is:

// Not allowed!
let string = String::from("hello, world!");
let view_string: = async move || -> &str { &string };

let fut = AsyncFnOnce::call_once(view_string, ())
// What does this even return? `view_string` was consumed by
// the call above, and the future was consumed by awaiting it.
// therefore `string` is no longer with is -- we can't take
// a reference to data that longer exists.
let _: /* &'? str */ = fut.await;

This is particularly important, since without this limitation, we'd be back at square zero, the general lending closure problem. This is enforced, specifically, by the fact that while the AsyncFnMut::CallMutFuture associated type has a GAT lifetime:

type CallMutFuture<'s>: Future<Output = Self::Output>
where
    Self: 's;

The AsyncFnOnce::Output type, which represents the value returned by awaiting the future does not:

type Output;

And both the AsyncFnOnce and AsyncFnMut implementation share this output type, even if they have different future types.

tl;dr

  • Async closures need to be lending, since they return futures that may borrow from the closure's captures.
  • So they need to implement some LendingFnMut trait.
  • However, LendingFnMut is not easy to introduce into the current Fn->FnMut->FnOnce trait hierarchy, because LendingFnMut cannot be a subtrait of FnOnce. Why? Because:
    • The value returned by a LendingFnMut implementation typically differs from that returned by FnOnce, but they're inherited by the current subtraits.
    • So let's split out these two types however, in general, there's no meaningful value for a lending closure to return when called in a way that consumes the closure.
    • That is: lending closures typically do not implement FnOnce at all.
    • So we can't just fix FnMut and make it "just work".
  • However, Async closures do have an obvious FnOnce implementation that can be derived programmatically from their LendingFnMut implementation.
  • To simplify closure signature inference, it's easiest to stop using the most general definition (LendingFnMut) and specialize this trait for async we introduce AsyncFn/AsyncFnMut/AsyncFnOnce.
  • But to keep space for future generalization, we can hide this behind an async trait bound modifier.

That leads us to the current design of async closures, which I believe are quite ergonomic and powerful. I recommend you try them out next time you're on nightly just write async before your Fn() trait bounds and use async || instead of || async and hopefully your errors will just disappear

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →


I didn't review this much after I wrote it, so I might make edits after the fact, and also I may have literally left a sentence out or something. Ping me if you want me to make a change!

Thanks for reading!


  1. https://github.com/rust-lang/rust/pull/120361 ↩︎

  2. https://github.com/rust-lang/rust/pull/120712 ↩︎

  3. https://github.com/rust-lang/rust/pull/123518 ↩︎

  4. https://github.com/rust-lang/rust/pull/125259 ↩︎

  5. The exact meaning of an impl Fn() -> impl Trait is a bit up in the air so we've hesitated to give it an exact meaning just yet. ↩︎

  6. You can come up with a reason why the accessor method is async maybe it needs to do a lookup in some some global service database or something. ↩︎

  7. You should read the section "Async closures are a lending pattern". ↩︎

  8. the FnOnce implementation for an FnMut closure will just dispatch to the FnMut impl. It's called the "FnOnce shim" in rustc parlance. ↩︎

  9. Well, in reality it's AsyncFnMut. More about that directly below. ↩︎