# Idea: unwinding and linear types in async context

**URL:** <https://internals.rust-lang.org/t/idea-unwinding-and-linear-types-in-async-context/22522>\
**Category:** language design\
**Created:** [March 8, 2025, 2:20pm UTC](https://internals.rust-lang.org/t/idea-unwinding-and-linear-types-in-async-context/22522 "2025-03-08T14:20:35Z")\
**Posts on this page:** 13\
**Page:** 1

<div class="post-metadata">

**Author:** ![Ddystopia](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/ddystopia/32/10915_2.png) [@Ddystopia](https://internals.rust-lang.org/u/Ddystopia)\
**Post date:** [March 8, 2025, 2:20pm UTC](https://internals.rust-lang.org/t/idea-unwinding-and-linear-types-in-async-context/22522/1 "2025-03-08T14:20:35Z")

</div>

One of the biggest problems with Linear/Undroppable types is unwinding - you cannot drop that type, so if you unwind, you cannot go forward. Currently there are 2 ideas: first is to abort the process, second is to have some kind of "no unwind" effect.

I think a third approach is possible, for futures. When sync code panics, there is not much you can do, as control flow is not in your hands. But with async it is a little bit different. I think it may be useful if `Future` that panicked during the `poll` will become `pending`, i.e. always return `Poll::Pending`. Then, with a trigger from the panic hook, it may be cancelled in some way.

So, what if a future containing undroppable types will handle panics like that? And if undroppable type is not held across `await` points, store it in the `Future` anyways.

I made a proof of concept crate [pending\_unwind](https://crates.io/crates/pending_unwind), but I think it may be possible to add another method for `Future` that will help recover from the panics with linear types present, maybe involving `async Drop`?

---

<div class="post-metadata">

**Author:** ![steffahn](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/steffahn/32/13288_2.png) [@steffahn](https://internals.rust-lang.org/u/steffahn)\
**Post date:** [March 8, 2025, 2:44pm UTC](https://internals.rust-lang.org/t/idea-unwinding-and-linear-types-in-async-context/22522/2 "2025-03-08T14:44:52Z")

</div>

> [@Ddystopia](#):
>
> I made a proof of concept crate [pending\_unwind](https://crates.io/crates/pending_unwind)

I can say with confidence that I don’t understand how this API is supposed to be used _even after_ reading your whole post here, the API docs, and the included test cases.

Maybe a good, illustrative example could help?

All I _can_ tell is that this `NoUnwind<Fut>`’s `Future` impl looks a lot like this goes against the general property/contract for well-behaved `Future`s that:

> If the future is not able to complete yet, it returns `Poll::Pending` and arranges for the `wake()` function to be called when the `Future` is ready to make more progress.

---

<div class="post-metadata">

**Author:** ![Jules-Bertholet](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/jules-bertholet/32/10671_2.png) [@Jules-Bertholet](https://internals.rust-lang.org/u/Jules-Bertholet)\
**Post date:** [March 8, 2025, 5:18pm UTC](https://internals.rust-lang.org/t/idea-unwinding-and-linear-types-in-async-context/22522/3 "2025-03-08T17:18:40Z")

</div>

IIUC: a future that panicked will never be able to make progress, so converting this into `Pending`-forever doesn’t violate the contract. The idea, I think, is that the panic hook would call `nesting_depth` and then alert the executor somehow if it is nonzero. The executor would then be charged with handling the situation.

---

<div class="post-metadata">

**Author:** ![NobodyXu](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/nobodyxu/32/9651_2.png) [@NobodyXu](https://internals.rust-lang.org/u/NobodyXu)\
**Post date:** [September 20, 2025, 11:49am UTC](https://internals.rust-lang.org/t/idea-unwinding-and-linear-types-in-async-context/22522/4 "2025-09-20T11:49:48Z")

</div>

The hardest part for adopting this is how middleware would handle it.

I.e. futures-utils, tokio::try\_join, tokio::select

Just returning Poll::Pending wouldn't work for them.

A better way would be to have a new method with default implementation:

```
trait Futures {
    // thread::Result is what catch_unwind returns
    fn poll_no_panick(self: Pin<&mut Self>, cx: Context<'_>) -> Poll<thread::Result<Self::Item>>;
}

```

---

<div class="post-metadata">

**Author:** ![Ddystopia](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/ddystopia/32/10915_2.png) [@Ddystopia](https://internals.rust-lang.org/u/Ddystopia)\
**Post date:** [September 20, 2025, 12:06pm UTC](https://internals.rust-lang.org/t/idea-unwinding-and-linear-types-in-async-context/22522/5 "2025-09-20T12:06:44Z")

</div>

That would probably be a performance suicide. And I'm not sure, what is the problem with joins and selects? If one of the futures is undroppable, their combination is undroppable too. For join it should be reasonable that if one of the futures diverges, the whole combined future will diverge. For selects: if one branch diverged and other returned a value, we wentured deep into the async drop territory

---

<div class="post-metadata">

**Author:** ![NobodyXu](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/nobodyxu/32/9651_2.png) [@NobodyXu](https://internals.rust-lang.org/u/NobodyXu)\
**Post date:** [September 20, 2025, 12:12pm UTC](https://internals.rust-lang.org/t/idea-unwinding-and-linear-types-in-async-context/22522/6 "2025-09-20T12:12:33Z")

</div>

> [@Ddystopia](#):
>
> For join it should be reasonable that if one of the futures diverges, the whole combined future will diverge

If you return Poll::Pending on panic, then join won't know that the future has panicked and need to call async drop, it would just keep polling it and it would not give the executor a chance to check if an exception has happened, unless it inserts a local thread check into its polling code somewhere, which is also expensive.

Essentially it introduces an implicit control flow that relies on thread-local global variables, instead of using Result, implicit exception handling is always hard to optimize and hard to reason than explicit Result control flow.

This is even harder to reason than throwing exception as it creates a control flow that seems normal, but somehow is actually handling an exception and doing rewind.

---

<div class="post-metadata">

**Author:** ![NobodyXu](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/nobodyxu/32/9651_2.png) [@NobodyXu](https://internals.rust-lang.org/u/NobodyXu)\
**Post date:** [September 20, 2025, 12:23pm UTC](https://internals.rust-lang.org/t/idea-unwinding-and-linear-types-in-async-context/22522/7 "2025-09-20T12:23:26Z")

</div>

Having a poll\_no\_panick also means that the future can runs its cleanup code itself.

The future that panicks would run its own cleanup code, before propagating it to its parent future to do the unwind.

It essentially eliminates the dependency of libunwind and replace it with rust generated async code, easier to optimize and reason for compiler, libunwind is actually kind of slow, complex, opaque and hard to optimize.

---

<div class="post-metadata">

**Author:** ![Ddystopia](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/ddystopia/32/10915_2.png) [@Ddystopia](https://internals.rust-lang.org/u/Ddystopia)\
**Post date:** [September 20, 2025, 12:52pm UTC](https://internals.rust-lang.org/t/idea-unwinding-and-linear-types-in-async-context/22522/8 "2025-09-20T12:52:33Z")

</div>

> [@NobodyXu](#):
>
> If you return Poll::Pending on panic, then join won't know that the future has panicked and need to call async drop, it would just keep polling it and it would not give the executor a chance to check if an exception has happened, unless it inserts a local thread check into its polling code somewhere, which is also expensive.

Who will keep polling it? `Join` is just a proxy that polls each joined future one time when it is polled itself. Or do you mean polling due to progress of other futures? That should be handled via executor. Reporting of the failure, as it have been mentioned earlier :

> The idea, I think, is that the panic hook would call nesting\_depth and then alert the executor somehow if it is nonzero. The executor would then be charged with handling the situation.

Yes, the control flow is not that simple, but this is _not_ an application logic thing. Results must be used for logic. Panics already can unwind at any point. We aren't returning results with possible panics because it would (1): make writing code a horrible experience (2): pollute abi with unnecessary details, slowing everything down.

Panic in a non-drop future is not a situation `Join` could adequately handle, so in this idea we offload it to the user and an executor, and each user can decide their own strategy.

For example, the executor can try free everything it could and leak the rest, not poll the future anymore, and before that the user code notifies the "team" that the bug occurred and servers leaked some memory.

---

<div class="post-metadata">

**Author:** ![NobodyXu](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/nobodyxu/32/9651_2.png) [@NobodyXu](https://internals.rust-lang.org/u/NobodyXu)\
**Post date:** [September 20, 2025, 1:00pm UTC](https://internals.rust-lang.org/t/idea-unwinding-and-linear-types-in-async-context/22522/9 "2025-09-20T13:00:29Z")

</div>

> [@Ddystopia](#):
>
> Who will keep polling it? `Join` is just a proxy that polls each joined future one time when it is polled itself. Or do you mean polling due to progress of other futures? That should be handled via executor. Reporting of the failure, as it have been mentioned earlier :

Join can be considered as a runtime, not just a simple proxy, sometimes even with starving prevention/fair scheduling.

It keeps polling until all futures are ready, so in your model, all the join implementation has to constantly check for if panics are raised

---

<div class="post-metadata">

**Author:** ![Ddystopia](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/ddystopia/32/10915_2.png) [@Ddystopia](https://internals.rust-lang.org/u/Ddystopia)\
**Post date:** [September 20, 2025, 1:09pm UTC](https://internals.rust-lang.org/t/idea-unwinding-and-linear-types-in-async-context/22522/10 "2025-09-20T13:09:08Z")

</div>

If `Join` won't be polled, futures will not be polled. And even if it is, panicked future will just return `Pending`, effectively making `Join` the same `Pending` future.

---

<div class="post-metadata">

**Author:** ![NobodyXu](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/nobodyxu/32/9651_2.png) [@NobodyXu](https://internals.rust-lang.org/u/NobodyXu)\
**Post date:** [September 20, 2025, 1:21pm UTC](https://internals.rust-lang.org/t/idea-unwinding-and-linear-types-in-async-context/22522/11 "2025-09-20T13:21:47Z")

</div>

That is the problem, async rust is cooperative, if the Join never returns, then the executor would never has a chance to check for panic.

If join does not check for panic, it'd take a long time before Join yields control, as it could be doing something else, where as previously it returns on panic.

This would be a shocking change in behavior, user would expect panic to propagate.

And not to mention that change along is breaking API, no one expects Future to return Pending on panic.

Having a new function that explicitly returns the panic payload would be much better than implicitly breaking existing code.

---

<div class="post-metadata">

**Author:** ![Ddystopia](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/ddystopia/32/10915_2.png) [@Ddystopia](https://internals.rust-lang.org/u/Ddystopia)\
**Post date:** [September 20, 2025, 1:22pm UTC](https://internals.rust-lang.org/t/idea-unwinding-and-linear-types-in-async-context/22522/12 "2025-09-20T13:22:54Z")

</div>

That's why I said that panic should be reported to the scheduler. And by the way, infinite loop requires something actively waking the executor, and there's nothing like this.

---

<div class="post-metadata">

**Author:** ![NobodyXu](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/nobodyxu/32/9651_2.png) [@NobodyXu](https://internals.rust-lang.org/u/NobodyXu)\
**Post date:** [September 20, 2025, 1:28pm UTC](https://internals.rust-lang.org/t/idea-unwinding-and-linear-types-in-async-context/22522/13 "2025-09-20T13:28:14Z")

</div>

> [@Ddystopia](#):
>
> And by the way, infinite loop requires something actively waking the executor

Sorry I was wrong about that, though it's still pretty confusing change and go against Rust's preference of explicitness over implictness
