rustmacrosrust-tokiorust-macrospanic

tokio::select! eager evaluation only if outside async block


I'm trying to use the select! macro's pre-condition to only include a branch if an Option is the Some variant.

I observe behavour which surprises me: the unwrap is eagerly evaluated causing panic! unless contained within async { }.

That is, this works as expected:

async fn foo(x: u32) {}
let maybe: Option<u32> = None;
select! {
    _ = async { foo(maybe.unwrap()).await }, if maybe.is_some() => (),
    _ = async { some_other_branch().await } => (),
}

...whereas this causes the thread to panic!:

async fn foo(x: u32) {}
let maybe: Option<u32> = None;
select! {
    _ = foo(maybe.unwrap()), if maybe.is_some() => (),
    _ = async { some_other_branch().await } => (),
}

What's the reason for the difference in behaviour here?


Solution

  • It is because in the panicking version, the expression foo(maybe.unwrap()) is eagerly evaluated (to a future), which will conditionally be awaited. But it is too late; simply constructing foo(maybe.unwrap()), in order that it may be awaited, panics because maybe is None.

    On the other hand, the future async { foo(maybe.unwrap()).await } doesn't panic (yet) because its body is only evaluated once it's awaited. And it will never be awaited because the precondition if maybe.is_some() is false.

    This is roughly analogous to the following code:

    fn f() {
        panic!()
    }
    fn g() {}
    
    let index = 1;
    
    // panics
    let result1 = [f(), g()][index];
    
    // this is fine; f() is never actually called
    let fns: [Box<dyn Fn()>; 2] = [Box::new(|| f()), Box::new(|| g())];
    let result2 = fns[index]();