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.
Table of Contents
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:
- Generate a
Future
that returns aString
output"bright"
. - Add
then()
combinator function to modify the original future result"bright"
to generate theFuture
result"not bright"
. - 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:
- Generate a
Future
that returns aOk(1)
output. - Use the
and_then()
combinator function to modify the original future resultOk(1)
to generate a newFuture
resultOk(x + 5)
. - Validate the modified
Future
result equalsOk(x + 5)
which isOk(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:
- Generate a
Future
that returns aOk(String::from("Expected Error"))
output. - Use the
or_else()
combinator function to modify the original future resultOk(String::from("Expected Error"))
to generate a newFuture
resultErr(format!("{} {}", String::from("[Error]"), x))
- Validate the modified
Future
result resolved value equalsErr(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 panics | It 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. |
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()
whenFuture
successfully resolves. - Use
and_then()
whenFuture
successfully resolves anOk
. - Use
or_else()
when Future successfully resolves anErr
.
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.