Return from `async move` on error

I wonder if there's a way to write this let else construct in a concise way using the ? operator:

async fn foo() -> Result<(), Box<dyn std::error::Error>> {
    Err("some error")?;
    Ok(())
}

#[tokio::main]
async fn main() {
    let join_handle = tokio::spawn(async move {
        loop {
            let Ok(()) = foo().await else { return; }; // any way to write this with `?`
        }
    });
    join_handle.await.unwrap();
}

(Playground)

The closest that I found is this, but it's still somewhat clunky:

#[tokio::main]
async fn main() {
    let join_handle = tokio::spawn(async move {
        loop {
            //let Ok(()) = foo().await else { return; }; // any way to write this with `?`
            foo().await.map_err(|_| ())?;
        }
        #[allow(unreachable_code)]
        Ok::<(), ()>(())
    });
    join_handle.await.unwrap().ok();
}

(Playground)

I'm not really interested in the error. It will be a SendError / RecvError from some channel(s). If I get the error, I want the task to end.

How's this?

    let join_handle = tokio::spawn(async move {
        while let Ok(()) = foo().await {}
    });
2 Likes

It solves my example, but doesn't allow me to capture the environment. (My fault for making a too simple example.)

impl<T> Nop<T>
where
    T: Message + Send + 'static,
{
    /// Creates a block which does nothing but pass data through
    pub fn new() -> Self {
        let (mut receiver, receiver_connector) = new_receiver::<T>();
        let (sender, sender_connector) = new_sender::<T>();
        spawn(async move {
            loop {
                let Ok(msg) = receiver.recv().await else { return; };
                // more stuff goes here, of which some needs access to
                // arguments passed to the `new` method later
                let Ok(()) = sender.send(msg).await else { return; };
            }
        });
        Self {
            receiver_connector,
            sender_connector,
        }
    }
}

This is more how it looks like, with at least sender and receiver being captured by the async move. In many cases I have more variables captured.

If I use a function, I cannot capture the environment.

An older (real life) version of the code demonstrating where I need this is here (edit: or look there for a more complex example). However, certain interfaces will change with the next version, so the above pasted snippet is closer to what I need (but I can't easily make a Playground example of either of it).


Side note: Switching to the let-else statements led to rustfmt not working properly yet (issue #4914 of rustfmt), i.e. it doesn't format the let-else statements yet.


I also tried this, but then have problems with futures not being Send:

async fn foo() -> Result<(), Box<dyn std::error::Error>> {
    Err("some error")?;
    Ok(())
}

#[tokio::main]
async fn main() {
    let join_handle = tokio::spawn(async move {
        loop {
            foo().await?;
        }
        #[allow(unreachable_code)]
        Ok::<(), Box<dyn std::error::Error>>(())
    });
    join_handle.await.unwrap().ok();
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0277]: `dyn std::error::Error` cannot be sent between threads safely
   --> src/main.rs:8:36
    |
8   |       let join_handle = tokio::spawn(async move {
    |  _______________________------------_^
    | |                       |
    | |                       required by a bound introduced by this call
9   | |         loop {
10  | |             foo().await?;
11  | |         }
12  | |         #[allow(unreachable_code)]
13  | |         Ok::<(), Box<dyn std::error::Error>>(())
14  | |     });
    | |_____^ `dyn std::error::Error` cannot be sent between threads safely
    |
    = help: the trait `Send` is not implemented for `dyn std::error::Error`
    = note: required for `Unique<dyn std::error::Error>` to implement `Send`
    = note: required because it appears within the type `Box<dyn std::error::Error>`
    = note: required because it appears within the type `Result<(), Box<dyn std::error::Error>>`
note: required by a bound in `tokio::spawn`
   --> /playground/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.21.2/src/task/spawn.rs:128:20
    |
128 |         T::Output: Send + 'static,
    |                    ^^^^ required by this bound in `tokio::spawn`

For more information about this error, try `rustc --explain E0277`.
error: could not compile `playground` due to previous error


I guess I could just pass all variables to a function, but this feels pretty verbose:

#[tokio::main]
async fn main() {
    let some_variable = ();
    let some_other_variable = ();
    let join_handle = tokio::spawn(async move {
        async fn inner(_some_variable: (), _some_other_variable:()) -> Result<(), Box<dyn Error>> {
            loop {
                foo().await?;
                bar().await?;
            }
        }
        inner(some_variable, some_other_variable).await.ok()
    });
    join_handle.await.unwrap();
}

(Playground)

Also, this performs an unnecessary heap allocation if I don't actually deal with boxed errors:

impl std::error::Error for SomeError {}
impl std::error::Error for AnotherError {}

async fn foo() -> Result<(), SomeError> {
    Err(SomeError)?;
    Ok(())
}

async fn bar() -> Result<(), AnotherError> {
    Ok(())
}

None of the variants really seem to be nice, and so far using let Ok(()) = … else { return; }; seems to be most concise :face_with_diagonal_mouth:.