# Lifetime GATs for oldschool async closures

**URL:** <https://users.rust-lang.org/t/lifetime-gats-for-oldschool-async-closures/139297>\
**Category:** uncategorized\
**Created:** [March 31, 2026, 5:59pm UTC](https://users.rust-lang.org/t/lifetime-gats-for-oldschool-async-closures/139297 "2026-03-31T17:59:17Z")\
**Posts on this page:** 5\
**Page:** 1

<div class="post-metadata">

**Author:** ![tvsfx](https://sea1.discourse-cdn.com/flex019/user_avatar/users.rust-lang.org/tvsfx/32/39199_2.png) [@tvsfx](https://users.rust-lang.org/u/tvsfx)\
**Post date:** [March 31, 2026, 5:59pm UTC](https://users.rust-lang.org/t/lifetime-gats-for-oldschool-async-closures/139297/1 "2026-03-31T17:59:17Z")

</div>

Hi, I'm having a problem mixing HRTBs with (old-style) async closures.

Context:

- [No syntactic way to specify GATs with where bounds in where clauses · Issue #95268 · rust-lang/rust · GitHub](https://github.com/rust-lang/rust/issues/95268)
- [The Better Alternative to Lifetime GATs - Sabrina Jewson](https://sabrinajewson.org/blog/the-better-alternative-to-lifetime-gats)

The second resource describes a workaround for the issue described in the first resource. It allows one to specify HRTBs of the form `for<'a where 'a: 'limit>` through an auxiliary trait. This works for structs, but I am trying to apply this same idea to closures that return futures ([playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=2a9349bb40dcce05a1fd3eeb21f14b37)):

```rust
use futures::future::BoxFuture;

/// Trait to enforce that 'limit : 'bounded
trait LtBound<'limit, 'bounded, _LifetimeConstraint = &'bounded &'limit ()>:
    FnOnce(&'bounded Arg) -> BoxFuture<'bounded, ()>
{}
impl<'limit, 'bounded, Impl> LtBound<'limit, 'bounded> for Impl where
    Impl: FnOnce(&'bounded Arg) -> BoxFuture<'bounded, ()>
{}

/// Trait to close over 'bounded
trait DelimitedFnOnce<'limit>: for<'bounded> LtBound<'limit, 'bounded> {}

impl<'limit, Impl> DelimitedFnOnce<'limit> for Impl where
    for<'bounded> Impl: LtBound<'limit, 'bounded>
{
}

struct Arg;
struct Captured;

/// Function that requires the bound.
fn update_arg<'a, F>(_captured: &'a Captured, _update_fun: F)
where
    F: DelimitedFnOnce<'a>,
{}

// Test: closure that closes over `&'a Captured` and passes 
// it to `update_arg`. This is conceptually fine given the bound
// `F: DelimitedFnOnce<'a>` (something something coercion sites)
fn test<'a>(s: &'a Captured) {
    update_arg::<'a>(s, move |_| {
        Box::pin(async move {
            let _s = s;
        })
    });
}

```

This fails with the error I was trying to avoid:

```rust
error: lifetime may not live long enough
  --> src/lib.rs:36:9
   |
34 | fn test<'a>(s: &'a Captured) {
   | -- lifetime `'a` defined here
35 | s.update_arg::<'a>(move |_| {
36 | / Box::pin(async move {
37 | | let _s = s;
38 | | })
   | | __________ ^ returning this value requires that `'a` must outlive `'static`

error: could not compile `playground` (lib) due to 1 previous error

```

I conceptually expect this to work because `DelimitedFnOnce<'a>` on `update_arg` basically says that our `FnOnce` can only be invoked with lifetimes `for<'b where 'a: 'b>`.  
I assume the reason this doesn't work is either that type inference first derives a higher-rank type for the closure using a unification variable, and fails before even considering `F: DelimitedFnOnce<'a>` on the function call, or that somehow the implicit bound is lost along the way.

---

<div class="post-metadata">

**Author:** ![quinedot](https://sea1.discourse-cdn.com/flex019/user_avatar/users.rust-lang.org/quinedot/32/17693_2.png) [@quinedot](https://users.rust-lang.org/u/quinedot)\
**Post date:** [March 31, 2026, 7:08pm UTC](https://users.rust-lang.org/t/lifetime-gats-for-oldschool-async-closures/139297/2 "2026-03-31T19:08:41Z")

</div>

I think two issues are that

- You'll probably need to drive closure inference by using a `: Fn*` bound, which are special (a subtrait of `Fn*` doesn't drive closure inference for example)
- Sometimes HRTBs impose `'static` in ways the alternative doesn't work around

The first point being something I've concretely wrestled with, and the second point being more of a feeling.

You can deal with the first bullet point to some extent by sneaking the implicit lifetime bound into the closure bound.

```rust
fn drive_inference<'limit, F>(f: F) -> F
where
    for<'bounded> F: FnOnce(
        &'bounded Arg,
        [&'bounded &'limit (); 0] // <-- for<'b where 'a: 'b> emulator
    ) -> BoxFuture<'bounded, ()>
{
    f
}

```

This gets you closer to your goal (I think):

> **[Rust Playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=d854ca9104a2803c33f4b35836167cb8)**
>
> A browser interface to the Rust compiler to experiment with the language

---

<div class="post-metadata">

**Author:** ![tvsfx](https://sea1.discourse-cdn.com/flex019/user_avatar/users.rust-lang.org/tvsfx/32/39199_2.png) [@tvsfx](https://users.rust-lang.org/u/tvsfx)\
**Post date:** [April 3, 2026, 12:07am UTC](https://users.rust-lang.org/t/lifetime-gats-for-oldschool-async-closures/139297/3 "2026-04-03T00:07:09Z")

</div>

Oh, I never realized the `Fn*`-traits had this interaction with wrapper traits during type inference; that's a good pointer, thanks! 🙂

I dont actually need the `DelimitedFnOnce` trait; I just introduced it to have a place to put the `'limit : 'bound` constraint, so the whole thing simplifies to: [Rust Playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=c01cbacd55090b8dd4e5407f6a7ecdc9)

It's a shame the extra argument is necessary, but on the other hand, no extra traits are required, so that's great.

I had hoped to build upon this by moving the argument type into the generic associated type of a trait that has the lifetime bound (so we don't need an extra arg to specify it), but unfortunately it seems like implicit bounds on associated types are not propagated: [Rust Playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=727d1c5470bbcb0d33dccc3a374342a7)

---

<div class="post-metadata">

**Author:** ![quinedot](https://sea1.discourse-cdn.com/flex019/user_avatar/users.rust-lang.org/quinedot/32/17693_2.png) [@quinedot](https://users.rust-lang.org/u/quinedot)\
**Post date:** [April 3, 2026, 12:48am UTC](https://users.rust-lang.org/t/lifetime-gats-for-oldschool-async-closures/139297/4 "2026-04-03T00:48:56Z")

</div>

You can get [a little further](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=1e7b9473ece0e21009e2432c3aa68cbe) (the implementations succeed but `test` fails) by adding explicit bounds to the GAT, which looks like the "you put a bound on a lifetime so HRTB fails" situation to me, and I think at this point we're basically circling back round to Sabrina's blog post.

Or [this](https://github.com/rust-lang/rust/issues/111158) issue or another one. Anyway, yeah, I haven't found a way around this issue either.

---

<div class="post-metadata">

**Author:** ![system](https://sea1.discourse-cdn.com/flex019/user_avatar/users.rust-lang.org/system/32/51372_2.png) [@system](https://users.rust-lang.org/u/system)\
**Post date:** [July 2, 2026, 12:49am UTC](https://users.rust-lang.org/t/lifetime-gats-for-oldschool-async-closures/139297/5 "2026-07-02T00:49:37Z")

</div>

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.
