joinrusttry-catchspawn

tokio::try_join! doesn't return early for tokio::spawn handlers


#[tokio::main]
async fn main() -> io::Result<()> {

// ...

let handle1 = tokio::spawn( async move {method1().await });
let handle2 = tokio::spawn( async move {method2().await });
let handle3 = tokio::spawn( async move {method3().await });

let (handle1_ret, handle2_ret, handle3_ret) = tokio::try_join!(handle1, handle2, handle3)?;

match handle1_ret {
    Ok(_) => info!("handle1 closed");
    Err(e) => error!("handle1 failed");
}

match handle2_ret {
    Ok(_) => info!("handle2 closed");
    Err(e) => error!("handle2 failed");
}

match handle3_ret {
    Ok(_) => info!("handle3 closed");
    Err(e) => error!("handle3 failed");
}

Ok(())

}

tokio::try_join! got stuck there even there is an error from method1/method2/method3.

What I want is if method1 or method2 or method3 got any error, tokio::try_join! should return and then the program should close (other running methods will terminate).


Solution

  • This is what try_join! is designed to do. The reason you aren't really seeing it is because your handle{1,2,3} tasks are actually returning Results of Results. Your method{1,2,3} calls may return an error but the tokio::spawn itself may also return an error (due to failure to spawn or task panic). So you really are .await-ing a Result<Result<_, _>, _> and try_join! will only handle the outer Result.

    To get try_join! to handle your method{1,2,3} errors, you'll need to "flatten" your Results. A fairly straightforward way to do that is to ignore any tokio::spawn errors since those are often unrecoverable:

    let handle1 = tokio::spawn(async move { method1().await });
    let handle2 = tokio::spawn(async move { method2().await });
    let handle3 = tokio::spawn(async move { method3().await });
    
    let (handle1_ret, handle2_ret, handle3_ret) = tokio::try_join!(
        async { handle1.await.unwrap() },
        async { handle2.await.unwrap() },
        async { handle3.await.unwrap() },
    )?;
    

    Or if you have a vacuous error type (Box<dyn Error>, anyhow, etc.) or otherwise has a From impl for JoinError you can just use ? of course:

    let (handle1_ret, handle2_ret, handle3_ret) = tokio::try_join!(
        async { handle1.await? },
        async { handle2.await? },
        async { handle3.await? },
    )?;
    

    Or really any other pattern matching on the Result via match, if let, or whatever will let you handle it however you need. It just needs to be in that async { } block you pass to try_join!.