macro_rules! join_then_try {
    (@ {
        // One `_` for each branch in the `join_then_try!` macro. This is not used once
        // normalization is complete.
        ( $($count:tt)* )

        // The expression `0+1+1+ ... +1` equal to the number of branches.
        ( $($total:tt)* )

        // Normalized join_then_try! branches
        $( ( $($skip:tt)* ) $e:expr, )*

    }) => { ... };
    (@ { ( $($s:tt)* ) ( $($n:tt)* ) $($t:tt)* } $e:expr, $($r:tt)* ) => { ... };
    ( $($e:expr),+ $(,)?) => { ... };
    () => { ... };
}
Expand description

Waits on multiple concurrent branches for all futures to complete, returning Ok(_) or an error.

Unlike tokio::try_join, this macro does not cancel remaining futures if one of them returns an error. Instead, this macro runs all futures to completion.

If more than one future produces an error, join_then_try! returns the error from the first future listed in the macro that produces an error.

The join_then_try! macro must be used inside of async functions, closures, and blocks.

Why use join_then_try?

Consider what happens if you’re wrapping a set of AsyncWriteExt::flush operations.

use tokio::io::AsyncWriteExt;

let temp_dir = tempfile::tempdir()?;
let mut file1 = tokio::fs::File::create(temp_dir.path().join("file1")).await?;
let mut file2 = tokio::fs::File::create(temp_dir.path().join("file2")).await?;

// ... write some data to file1 and file2

tokio::try_join!(file1.flush(), file2.flush())?;

If file1.flush() returns an error, file2.flush() will be cancelled. This is not ideal, since we’d like to make an effort to flush both files as far as possible.

One way to run all futures to completion is to use the tokio::join macro.

let mut file1 = tokio::fs::File::create(temp_dir.path().join("file1")).await?;
let mut file2 = tokio::fs::File::create(temp_dir.path().join("file2")).await?;

// tokio::join! is unaware of errors and runs all futures to completion.
let (res1, res2) = tokio::join!(file1.flush(), file2.flush());
res1?;
res2?;

This, too, is not ideal because it requires you to manually handle the results of each future.

The join_then_try macro behaves identically to the above tokio::join example, except it is more user-friendly.

let mut file1 = tokio::fs::File::create(temp_dir.path().join("file1")).await?;
let mut file2 = tokio::fs::File::create(temp_dir.path().join("file2")).await?;

// With join_then_try, if one of the operations errors out the other one will still be
// run to completion.
cancel_safe_futures::join_then_try!(file1.flush(), file2.flush())?;

If an error occurs, the error from the first future listed in the macro that errors out will be returned.

Notes

The supplied futures are stored inline. This macro is no-std and no-alloc compatible and does not require allocating a Vec.

This adapter does not expose a way to gather and combine all returned errors. Implementing that is a future goal, but it requires some design work for a generic way to combine errors. To do that today, use tokio::join and combine errors at the end.

Runtime characteristics

By running all async expressions on the current task, the expressions are able to run concurrently but not in parallel. This means all expressions are run on the same thread and if one branch blocks the thread, all other expressions will be unable to continue. If parallelism is required, spawn each async expression using tokio::task::spawn and pass the join handle to join_then_try!.