Rust | Difference between then() vs and_then() vs or_else()

As you learn Rust, you find out more and more functions that sound like they behave in a similar way, when in reality they differ in subtle ways. This is the case for the functions then() , and_then() , and or_else(). No worries, this article will cover each of these methods as well as their differences.

The functions then() , which is part of theFutureExt trait,and_then() and or_else() , which are part of the TryFutureExt trait, are combinators that allow building complex values of based on a successfully completing a Future. These functions are accessible to the Future trait because FutureExt and TryFutureExt traits are extensions of the Future trait.

The previous definition sounds complex and hard to understand, especially after mentioning the term “combinators”. Don’t worry, in the next sections you will learn what a combinator is and how to use then() , and_then(), and or_else() functions.

What is a combinator?

In Rust, there is not a clear definition for combinators. However, I will share my interpretation of this definition. Hence, I encourage doing additional research if you need more clarity.

The term combinator exists as Rust provides combinator functions. Combinator functions are available to build complex fragments of a program from an initial flow or process. This means, extending the behavior of a program based on the result of an existing process.

To make things more clear let’s analyze the following scenario. A program has a process to calculate two numbers in function sum(a:u8, b:u8). a + b produces c. The result of c will always be the same unless the values a or b change.

If the program needs to multiply d times the result of a + b , you could add to the logic of (a + b ) * d to the sum function. However, this will no longer represent a true generic sum.

Ideally, you will keep the sum function as it is (a + b , which equals c) and trigger another logic that does c * d right after executing the sum process.

This is where the term combinator function starts making its introduction as the final result will no longer be c, but instead the combination of two processes on a type T( in this case, the type u8) that generates the result of c * d, which ends up being e.

In this scenario, we are introducing only a combinator function. Combinator functions could represent in this example additional mathematical operations such as subtracting, dividing, generating percentages, generating ratios, etc. Meaning that you could introduce as many combinator functions as possible that modify the result of a given type T.

How then() works

Different from and_then() and or_else(), the then() function is part of the FutureExt trait.

The then() combinator function can execute whenever a Future value becomes available after a successful completion of the Future. In other words, the logic inside the then() function is triggered once the execution of an asynchronous operation successfully completes.

You can find the function definition of then() in here or in the following code snippet,

fn then<Fut, F>(self, f: F) -> Then<Self, Fut, F>
    where
        F: FnOnce(Self::Output) -> Fut,
        Fut: Future,
        Self: Sized,
    {
        assert_future::<Fut::Output, _>(Then::new(self, f))
    }

which f is a closure. Hence, to use the then() function, you need to pass a closure.

Below you will find an example of using the then() function.

use futures::future::FutureExt;

#[tokio::main]
async fn main() {
    test_then_method().await;
}

async fn test_then_method() {
    let my_future_is_bright = async { String::from("bright") };
    let my_future_is_not_bright =
        my_future_is_bright.then(|x| async move { format!("{} {}", String::from("not"), x) });

    assert_eq!(my_future_is_not_bright.await, String::from("not bright"));
}

Note: Add futures = "0.3" and tokio = { version = "1", features = ["full"] } dependencies in the Cargo.toml file if you want to execute the run the previous code snippet.

What happens in test_then_method function is:

  1. Generate a Future that returns a String output "bright".
  2. Add then() combinator function to modify the original future result "bright" to generate the Future result "not bright" .
  3. Validate the modified Future result equals "not bright" .

It is important to reiterate that the closure passed to the then() function will only execute if the previous Future successfully completes its asynchronous operation. Otherwise, that closure will never be executed.

Notice the import futures::future::FutureExt; . Failing to import the FutureExt trait will result in compilation errors as the then() function won’t be available to the my_future_is_bright future.

How and_then() works

The and_then() function is available when implementing the TryFutureExt trait.

Then and_then() combinator function executes another Future if a Future successfully completes its asynchronous operations and the value resolved is Ok .

You can find the function definition of and_then() in here or in the following code snippet,

fn and_then<Fut, F>(self, f: F) -> AndThen<Self, Fut, F>
    where
        F: FnOnce(Self::Ok) -> Fut,
        Fut: TryFuture<Error = Self::Error>,
        Self: Sized,
    {
        assert_future::<Result<Fut::Ok, Fut::Error>, _>(AndThen::new(self, f))
    }

in which f is a closure. Hence, to use the and_then() function, you need to pass a closure.

Below you will find an example of using the and_then() function.

use futures::TryFutureExt;

#[tokio::main]
async fn main() {
    test_and_then_method().await;
}

async fn test_and_then_method() {
    let ok_future = async { Ok::<u8, u8>(1) };
    let updated_ok_future = ok_future.and_then(|x| async move { Ok(x + 5) });

    assert_eq!(updated_ok_future.await, Ok(6));
}

Note: Add futures = "0.3" and tokio = { version = "1", features = ["full"] } dependencies in the Cargo.toml file if you want to execute the run the previous code snippet.

What happens in test_and_then_method function is:

  1. Generate a Future that returns a Ok(1)output.
  2. Use the and_then() combinator function to modify the original future result Ok(1) to generate a newFuture result Ok(x + 5) .
  3. Validate the modified Future result equals Ok(x + 5) which is Ok(6) .

Once again, the closure passed to the and_then() function will only execute if the previous Future successfully completes its asynchronous operation. Otherwise, that closure will never be executed.

Also, the closure passed to the and_then() must resolve an Ok value.

Notice the import futures::future::TryFutureExt; . Failing to import the TryFutureExt trait will result in compilation errors as the and_then() function won’t be available to the ok_future future.

How or_else() works

The or_else() function is available when implementing the TryFutureExt trait.

Similar to the then() and and_then() functions, the or_else() function executes whenever the another Future successfully resolves after completing the asynchronous operation. The resolved value must be an Err.

You can find the function definition of or_else() in here or in the following code snippet,

fn or_else<Fut, F>(self, f: F) -> OrElse<Self, Fut, F>
    where
        F: FnOnce(Self::Error) -> Fut,
        Fut: TryFuture<Ok = Self::Ok>,
        Self: Sized,
    {
        assert_future::<Result<Fut::Ok, Fut::Error>, _>(OrElse::new(self, f))
    }

which f is a closure. Hence, to use the or_else() function, you need to pass a closure.

Below you will find an example of using the or_else() function.

use futures::{future::err, TryFutureExt};

#[tokio::main]
async fn main() {
    test_or_else_method().await;
}

async fn test_or_else_method() {
    let err_future = err::<String, String>(String::from("Expected Error"));
    let updated_err_future = err_future.or_else(|x| async move {
       Err(
        format!("{} {}", String::from("[Error]"), x)
       )
    });

    assert_eq!(updated_err_future.await, Err(String::from("[Error] Expected Error")));
}

Note: Add futures = "0.3" and tokio = { version = "1", features = ["full"] } dependencies in the Cargo.toml file if you want to execute the run the previous code snippet.

What happens in test_and_then_method function is:

  1. Generate a Future that returns a Ok(String::from("Expected Error"))output.
  2. Use the or_else() combinator function to modify the original future result Ok(String::from("Expected Error")) to generate a newFuture resultErr(format!("{} {}", String::from("[Error]"), x))
  3. Validate the modified Future result resolved value equals Err(String::from("[Error] Expected Error") .

Remember, the closure passed to the or_else() function will only execute if the previous Future successfully completes its asynchronous operation and the value resolved is an Err. Otherwise, that closure will never be executed.

Notice the import futures::future::TryFutureExt; . Failing to import the TryFutureExt trait will result in compilation errors as the or_else() function won’t be available to the err_future future.

Differences between then() vs and_then() vs or_else()

then()and_then()or_else()
Implemented by the FutureExt trait.Implemented by the TryFutureExt trait.Implemented by the TryFutureExt trait.
Execution of the closure f as long as the previous Future successfully completes the asynchronous operation.Execution of the closure f as long as the previous Future successfully completes the asynchronous operation and resolves a value Ok.Execution of the closure f as long as the previous Future successfully completes the asynchronous operation and resolves a value Err.
It will never execute if previous Future panicsIt will never execute if previous Future panics, resolves an Err , or is dropped.It will never execute if previous Future panics, resolves Ok , or is dropped.
The closure f can resolve a different value type from the original Future.The closure f must resolve an Ok value.The closure f must resolve an Err value.
comparison table of then(), and_then(), and or_else() functions

Conclusion

All in all, this article explained the differences between combinator functions then(), and_then() , and or_else(). In short, all of the functions will trigger a closure f as long as the previous future successfully executes its asynchronous operation. However, the execution closure f is dependent on the value resolved from the previous future:

  • Use then() when Future successfully resolves.
  • Use and_then() when Future successfully resolves an Ok .
  • Use or_else() when Future successfully resolves an Err.

If you need to check the repository with the code examples used in this article, click the following link:

https://github.com/arealesramirez/rust-then–vs-and_then–vs_or_else-

Did you learn the differences between then() , and_then() , and or_else()?

Let me know by sharing your comments on Twitter of Become A Better Programmer or to my personal Twitter account.