[Discussion] Alternative syntax for async iteration: `for async i in stream`?

Hi everyone,

While the community has been gravitating towards for await i in stream, I’d like to propose considering for async i in stream instead.

My core argument is about declarative vs. imperative mental models:

  1. Symmetry with Async Closures/Blocks: We already use async move || {} and async {} to define asynchronous contexts. Using for async i treats the item i as an async binding. It signals that the source is asynchronous by nature, rather than just adding an imperative "wait" step at the top of the loop.

  2. Patterns, not Actions: for await feels like a command (loop and wait). for async i feels like a pattern (for each async item). This aligns better with potential future features like let async x = ... (async pattern matching or async drop).

  3. Reflecting the Stream's Nature: Just as async fn describes a function that yields a future, for async describes a loop that consumes an asynchronous producer. It keeps the syntax focused on the type of iteration rather than the low-level polling mechanism, staying true to Rust's goal of "zero-cost abstractions" for the mind.

I feel for async is more "Rust-y" in its declarativeness and provides a more cohesive aesthetic with the rest of the asyncecosystem.

What do you think? Does for async capture the intent of stream processing better than for await?

----

Quick note on context: I’m aware of the previous discussions comparing async for and for await (e.g., withoutboats' blog post). However, I want to clarify that for async i is distinct from the older async for proposal.

While async for was often discussed as a way to define the entire loop context, my proposal focuses on async pattern matching/binding at the item level. It aims to bridge the gap between "how we poll" (await) and "what the data is" (async). By using for async, we treat asynchrony as a property of the iteration binding, which I believe avoids some of the syntactic weight of for await while remaining more declarative than the "block-level" async for.

The await in for await is trying to suggest a very specific meaning: "there is a suspension point here, unwrapping a future (and possibly pausing execution)". Similarly, the async in async fn and the hypothetical async let [sic; a reference to a Swift feature rather than destructuring] has the opposite meaning: "there's an implicit async block here, wrapping computation into a future".

for async would make sense to me if you still have to i.await in the body of the loop…but if that were the case, how would the loop ever know when to terminate? Either something has to be awaited before the body begins, or the signature of next would need to be Option<Future<…>> rather than Future<Option<…>>, which would be less useful. So I think for await remains the correct choice.

3 Likes

Thanks for the feedback!

I agree that in async fn or async {}, async acts as a "wrapper." However, I’d argue that in the context of a binding site (like for <pattern> in <stream>), async can be naturally extended to mean "this binding is resolved asynchronously."

A few counter-points to consider:

  1. Async Patterns vs. Async Wrappers: In a for loop, the i is a pattern. Just as &i in a loop destructures a reference, async i could be seen as "destructuring" (or resolving) an asynchronous value. It tells the compiler: "The source is async, and we are binding the resolved value to i."

  2. The "Wait" is implicit in for: The for keyword already implies a sequential progression. Adding await feels like describing how the engine works (imperative), whereas async describes the nature of the data being bound (declarative).

  3. Avoiding the "Future of Option" confusion: You mentioned Option<Future<...>>. My proposal doesn't change the underlying AsyncIterator trait (which remains Future<Output = Option<T>>). Instead, it’s about how we denote the suspension point.

    • for await i: "Wait, then bind."

    • for async i: "Bind the result of an async step to i."

If we eventually get let async i = ... for destructuring futures or handling async drop, for async i becomes perfectly consistent. It marks the boundary where asynchrony is handled, rather than just being a label for the await operation itself.

FWIW, other languages also uses await here. For example, await foreach in C#: https://learn.microsoft.com/en-us/archive/msdn-magazine/2019/november/csharp-iterating-with-async-enumerables-in-csharp-8#a-tour-through-async-enumerables.

async is for "this is a thunk", but that's not what's happening in a for loop.

1 Like

Fair point on C#, but Rust's design philosophy often excels when it diverges from other languages to favor its own strengths: patterns and bindings.

While await describes an imperative action, async in for async i describes the nature of the binding. In Rust, where we use patterns like for &x in ..., treating asynchrony as a property of the data flow (the binding i) feels more "Rust-y" and forward-looking than just adding an imperative await keyword because others did.

Let's prioritize semantic consistency with Rust's own async ecosystem over following the await foreach precedent.

I hope you see the contraddiction here: the former yields while the other consumes (just like .await)

The issue here is that streams are not Iterator<Item = impl Future> where you can just await each item. You have to instead await getting the item itself. For this reason they cannot be fit into the existing for machinery, and adding a new kind of binding won't change this.

Can we please avoid generating posts/answers using LLMs? Or if you really want to do so at least remove the blatant parts.

5 Likes

:sweat_smile: yeah sorry. I'm not English native so helped with LLM to describe my opinion. sorry for that.

Edited.