Async function returning `+ Send`: compile error using `match` and `tokio::time::sleep`, but `if let` works

⚓ rust    📅 2025-05-16    👤 surdeus    👁️ 4      

surdeus

Warning

This post was published 45 days ago. The information described in this article may have changed.

Hello everyone,

I've run into a puzzling compilation error when implementing a trait method which returns impl Future<Output = Self> + Send. If the implementation of this trait method calls a function, that returns Result<_, Box<dyn std::error::Error>>, and handles both cases of the its result, using tokio::time::sleep in the error branch causes a compilation error. And if this wouldn't be strange enough on its own, there is a difference between using match and if let ... which affects the error as well.

Below a few code snippets illustrate the problem I just tried to describe:
The basic setup is a function which returns a boxed error and a trait containing an async function with the Send trait bound on its return type:

/// A function which returns a `Box<dyn std::error::Error>`.
async fn test() -> Result<(), Box<dyn std::error::Error>> {
    Ok(())
}

/// A trait with a method, which has a `Send` trait bound on its return type.
/// Removing this `Send` trait bound, makes all examples below compile, but
/// in my use case the trait is defined in another crate.
trait MyTrait {
    fn my_func() -> impl Future<Output = Self> + Send;
}

Trying to implement this trait like shown below, causes a compilation error:

struct ThisFails;

impl MyTrait for ThisFails {
    async fn my_func() -> Self {
        // Call `test()` function and work with its result...
        match test().await {
            Ok(_) => {},
            Err(_) => {
                // commenting this out resolves the compile error
                tokio::time::sleep(std::time::Duration::from_secs(1)).await;
            },
        }

        todo!()
    }
}

As the comment states, removing tokio::time::sleep makes the code compile.

Using the if let ... syntax instead of a match on the other hand, works fine and the code compiles:

struct ThisWorks;

impl MyTrait for ThisWorks {
    async fn my_func() -> Self {
        // Call `test()` function and work with its result...
        if let Ok(_) = test().await {

        } else {
            tokio::time::sleep(std::time::Duration::from_secs(1)).await;
        }

        todo!()
    }
}

But switching the order of the if let ... branches, causes the compile error to occur again:

struct ThisFails2;

impl MyTrait for ThisFails2 {
    async fn my_func() -> Self {
        // Call `test()` function and work with its result...
        if let Err(_) = test().await {
            // commenting this out resolves the compile error
            tokio::time::sleep(std::time::Duration::from_secs(1)).await;

        } else {

        }

        todo!()
    }
}

In all cases the error text says:

error: future cannot be sent between threads safely
  --> examples/weird-error/main.rs:34:5
   |
34 |     async fn my_func() -> Self {
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^ future returned by `my_func` is not `Send`
   |
   = help: the trait `Send` is not implemented for `dyn std::error::Error`
note: future is not `Send` as this value is used across an await
  --> examples/weird-error/main.rs:38:67
   |
36 |         if let Err(_) = test().await {
   |                         ------------ has type `Result<(), Box<dyn std::error::Error>>` which is not `Send`
37 |             // commenting this out resolves the compile error
38 |             tokio::time::sleep(std::time::Duration::from_secs(1)).await;
   |                                                                   ^^^^^ await occurs here, with `test().await` maybe used later
note: required by a bound in `MyTrait::{synthetic#0}`
  --> examples/weird-error/main.rs:10:50
   |
10 |     fn my_func() -> impl Future<Output = Self> + Send;
   |                                                  ^^^^ required by this bound in `MyTrait::{synthetic#0}`

I've also prepared a Rust Playground for everyone who wants to run the code and see the error in action :smiley:

My questions about this issue are:

  1. What does tokio::time::sleep do to make the Future returned by the my_func implementation not to be Sync anymore? Is this special to the tokio::time::sleep function or can other function calls cause the same behavior?
  2. Is there a fundamental difference between match and if let which causes them to have different effects in this case? If so, how does this explain their differences in combination with tokio::time::sleep?

Thank you very much in advance for your help! I'm really looking forward to hearing what you'll say about this problem :slight_smile:

3 posts - 3 participants

Read full topic

🏷️ rust_feed