What's the difference between `|x| async { x }` and `async |x| x`?

And why does one work, and the other one complains about async closures being unstable?

✅

let c1 = |x: i32| x + 1;
let c2 = |x: i32| async { x + 1 };

let captured = "5".to_string();
let c3 = |x: i32| async move {
  let m = captured;
  if let Ok(n) = captured.parse::<i32>() 
    { x + n } else { x }
};

❌ // what's so different about these?

let captured = "3".to_string();
let c4 = async |x: u32| { x + 1 };
let c5 = async move |x: u32| { x + 1 
  + captured.parse::<u32>().unwrap() };

Well, |x| async { x } is not really a single thing. It's an ordinary closure that happens to return an async block. It is stable now because both closures and async blocks are stable.

As for async |x| x, well that is a special unstable syntax for async closures. These exist as a distinct thing because the intention is that they will have better behavior lifetime-wise, but that is still work-in-progress.

4 Likes

Another clarification needed: move |x| async vs |x| async move vs move |x| async move - how are they different and what happens in each case? (Is there a book or any other resource about this?)

There's a resource that talks about what it means to put move on a closure here: Closures: Magic Functions. Putting move on an async block has more or less exactly the same effect.

The hard part about understanding what happens here is perhaps that even if you understand what it means on each piece, the consequences that this has on the combined closure-that-returns-async-block construct are not obvious.

  • With move |x| async { .. }, the closure will capture ownership, but when you call it, the async block is not given ownership. This usually doesn't work because if the async block only has a reference to the things it captures, then you can't return it.
  • With |x| async move { .. }, the closure wont capture ownership but the async block will. Usually this will work if the async block only uses x, but not if it uses any variables from outside the closure.
  • With move |x| async move { .. } things are moved to the closure, and when you call it, the closure passes it on to the async block with another move.
3 Likes

Awesome, amazingly helpful as usual.

One tricky case that appears somewhat regularly is when you have a type like Arc<T> that must be cloned to share access across multiple async tasks or threads. Then you often need to move the original into the closure, clone it each time the closure runs, and move the cloned pointer into the async block:

move || {
    let my_arc = my_arc.clone();
    async move {
        // Do stuff with `my_arc`.
    }
}

For a real-world example of this, see this comment or this one.

3 Likes

Btw, because of the rules for "inferred captures" (move-less closures / async blocks), a nifty trick is that you can skip the first move when doing move |…| async move { … } :upside_down_face: so as to only write:

|…| async move { … }
  • only when async follows the closure args

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.