Loading page…
Rust walkthroughs
Loading page…
futures::future::BoxFuture differ from Pin<Box<dyn Future>> and when would you use each?futures::future::BoxFuture<'a, T> is a type alias for Pin<Box<dyn Future<Output = T> + Send + 'a>> that provides a convenient shorthand for the most common boxed future pattern. The key difference is ergonomics: BoxFuture includes the Send bound by default and handles lifetime annotations, while Pin<Box<dyn Future>> requires explicit type annotations for the same effect. Use BoxFuture when you need a Send boxed future with standard lifetime semantics. Use Pin<Box<dyn Future>> directly when you need !Send futures, custom lifetime bounds, or when you want to be explicit about the boxing semantics.
use std::future::Future;
use std::pin::Pin;
use futures::future::BoxFuture;
// BoxFuture is defined as:
// pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
// These are equivalent:
fn with_boxfuture() -> BoxFuture<'static, String> {
Box::pin(async { "hello".to_string() })
}
fn with_pin_box() -> Pin<Box<dyn Future<Output = String> + Send + 'static>> {
Box::pin(async { "hello".to_string() })
}BoxFuture is a type alias that expands to the full Pin<Box<dyn Future...>> type.
use std::future::Future;
use std::pin::Pin;
use futures::future::BoxFuture;
// BoxFuture includes Send by default
fn send_future() -> BoxFuture<'static, i32> {
Box::pin(async { 42 })
}
// Pin<Box<dyn Future>> without Send - different type!
fn non_send_future() -> Pin<Box<dyn Future<Output = i32>>> {
// This future is NOT Send (no + Send in the type)
Box::pin(async { 42 })
}
// To match BoxFuture's behavior with Pin<Box<...>>:
fn explicit_send() -> Pin<Box<dyn Future<Output = i32> + Send + 'static>> {
Box::pin(async { 42 })
}BoxFuture adds Send automatically; Pin<Box<dyn Future>> does not.
use futures::future::BoxFuture;
// Storing futures in a collection
struct TaskManager {
// Clean and readable with BoxFuture
tasks: Vec<BoxFuture<'static, ()>>,
}
impl TaskManager {
fn add_task(&mut self, task: BoxFuture<'static, ()>) {
self.tasks.push(task);
}
fn spawn(&mut self) {
// Easy to store boxed futures
self.tasks.push(Box::pin(async {
println!("Task running");
}));
}
}
// Without BoxFuture, you'd write:
use std::pin::Pin;
use std::future::Future;
struct TaskManagerVerbose {
tasks: Vec<Pin<Box<dyn Future<Output = ()> + Send + 'static>>>,
}BoxFuture reduces verbosity when Send is required.
use std::pin::Pin;
use std::future::Future;
use futures::future::BoxFuture;
// Case 1: Non-Send futures
fn non_send_example() -> Pin<Box<dyn Future<Output = String>>> {
// Rc is not Send, so the future can't be Send
let data = std::rc::Rc::new("data".to_string());
Box::pin(async move {
data.as_ref().clone()
})
// BoxFuture<'static, String> would not compile here
// because the future is !Send
}
// Case 2: Custom lifetime bounds
fn custom_lifetime<'a>(data: &'a str) -> Pin<Box<dyn Future<Output = &'a str> + 'a>> {
Box::pin(async move {
data
})
}
// Case 3: Explicit about what you're boxing
fn explicit_boxing() -> Pin<Box<dyn Future<Output = i32> + Send + Sync + 'static>> {
// Adding additional bounds like Sync
Box::pin(async { 42 })
}Use Pin<Box<dyn Future>> when you need !Send futures or custom bounds.
use futures::future::BoxFuture;
use std::pin::Pin;
use std::future::Future;
// BoxFuture with lifetime - captures reference
fn borrow_data<'a>(data: &'a Vec<String>) -> BoxFuture<'a, usize> {
Box::pin(async move {
data.len()
})
}
// Equivalent with full type
fn borrow_data_verbose<'a>(data: &'a Vec<String>) -> Pin<Box<dyn Future<Output = usize> + Send + 'a>> {
Box::pin(async move {
data.len()
})
}
// Usage
async fn use_borrowed() {
let data = vec!["a".to_string(), "b".to_string()];
let future = borrow_data(&data);
let len = future.await;
println!("Length: {}", len);
}Both handle lifetimes identically; BoxFuture is just shorter.
use futures::future::BoxFuture;
// Recursive async functions need BoxFuture (or similar)
// because the future size is infinite at compile time
fn recursive_fib(n: u32) -> BoxFuture<'static, u64> {
Box::pin(async move {
if n <= 1 {
n as u64
} else {
recursive_fib(n - 1).await + recursive_fib(n - 2).await
}
})
}
// Without boxing, this wouldn't compile:
// async fn recursive_fib(n: u32) -> u64 {
// if n <= 1 { n as u64 }
// else { recursive_fib(n - 1).await + recursive_fib(n - 2).await }
// // Error: recursive async function returns a future that
// // contains itself, making its size infinite
// }BoxFuture enables recursive async patterns by hiding the infinite type behind a pointer.
use futures::future::BoxFuture;
use std::pin::Pin;
use std::future::Future;
// Storing different future types in a collection
trait Processor {
// Using BoxFuture in trait definitions
fn process(&self, input: String) -> BoxFuture<'static, String>;
}
struct UpperProcessor;
impl Processor for UpperProcessor {
fn process(&self, input: String) -> BoxFuture<'static, String> {
Box::pin(async move { input.to_uppercase() })
}
}
struct ReverseProcessor;
impl Processor for ReverseProcessor {
fn process(&self, input: String) -> BoxFuture<'static, String> {
Box::pin(async move { input.chars().rev().collect() })
}
}
async fn use_processors() {
let processors: Vec<Box<dyn Processor>> = vec![
Box::new(UpperProcessor),
Box::new(ReverseProcessor),
];
for processor in processors {
let result = processor.process("hello".to_string()).await;
println!("{}", result);
}
}BoxFuture works naturally in trait definitions for dynamic dispatch.
use futures::future::BoxFuture;
use std::pin::Pin;
use std::future::Future;
// Function returning boxed future - BoxFuture is clearer
fn get_data() -> BoxFuture<'static, String> {
Box::pin(async { "data".to_string() })
}
// Same function with full type
fn get_data_verbose() -> Pin<Box<dyn Future<Output = String> + Send + 'static>> {
Box::pin(async { "data".to_string() })
}
// Function with lifetime - BoxFuture is much clearer
fn with_ref<'a>(s: &'a str) -> BoxFuture<'a, String> {
Box::pin(async move { s.to_string() })
}
// Same with full type
fn with_ref_verbose<'a>(s: &'a str) -> Pin<Box<dyn Future<Output = String> + Send + 'a>> {
Box::pin(async move { s.to_string() })
}BoxFuture significantly reduces type annotation overhead.
use std::rc::Rc;
use std::cell::RefCell;
use futures::future::BoxFuture;
use std::pin::Pin;
use std::future::Future;
// This won't compile with BoxFuture
// fn bad_boxfuture() -> BoxFuture<'static, i32> {
// let rc = Rc::new(42);
// Box::pin(async move { *rc })
// // Error: Rc<i32> cannot be sent between threads safely
// }
// Must use Pin<Box<dyn Future>> without Send
fn non_send_future() -> Pin<Box<dyn Future<Output = i32>>> {
let rc = Rc::new(42);
Box::pin(async move { *rc })
}
// Similarly with RefCell
fn with_refcell() -> Pin<Box<dyn Future<Output = i32>>> {
let cell = RefCell::new(vec![1, 2, 3]);
Box::pin(async move {
cell.borrow().len() as i32
})
}!Send types require Pin<Box<dyn Future>> without the Send bound.
use futures::future::LocalBoxFuture;
// LocalBoxFuture is the !Send equivalent of BoxFuture
// It's defined as:
// pub type LocalBoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + 'a>>;
fn local_future() -> LocalBoxFuture<'static, i32> {
let rc = std::rc::Rc::new(42);
Box::pin(async move { *rc })
}
// So the naming convention is:
// - BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>
// - LocalBoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + 'a>>LocalBoxFuture is the non-Send version of BoxFuture.
use futures::future::BoxFuture;
// Boxing allocates on the heap
fn boxed_future() -> BoxFuture<'static, i32> {
// Heap allocation for the future state
Box::pin(async { 42 })
}
// Compare to static dispatch (no allocation)
async fn static_future() -> i32 {
42
}
// When boxing is necessary:
// 1. Recursive async (infinite type size)
// 2. Hiding future type (trait objects)
// 3. Storing futures in collections
// 4. Returning futures from functions where type is too complex
// When to avoid boxing:
// 1. Performance-critical paths
// 2. Known future types
// 3. Can use impl Future<Output = T>Boxing has allocation overhead; use it when necessary.
use futures::future::BoxFuture;
// impl Future - static dispatch, no allocation
fn static_dispatch() -> impl Future<Output = i32> {
async { 42 }
}
// BoxFuture - dynamic dispatch, heap allocation
fn dynamic_dispatch() -> BoxFuture<'static, i32> {
Box::pin(async { 42 })
}
// Use impl Future when:
// - Returning a single async block
// - Type is concrete and visible to caller
// - No recursion
// - No need to store in collection
// Use BoxFuture when:
// - Returning different future types
// - Implementing traits with async methods
// - Recursive functions
// - Storing futures in collectionsimpl Future avoids boxing when possible; BoxFuture enables type erasure.
use futures::future::BoxFuture;
use std::collections::VecDeque;
struct TaskQueue {
tasks: VecDeque<BoxFuture<'static, ()>>,
}
impl TaskQueue {
fn new() -> Self {
Self { tasks: VecDeque::new() }
}
fn add<F>(&mut self, task: F)
where
F: std::future::Future<Output = ()> + Send + 'static,
{
// BoxFuture makes it easy to store different future types
self.tasks.push_back(Box::pin(task));
}
async fn run_next(&mut self) -> bool {
if let Some(task) = self.tasks.pop_front() {
task.await;
true
} else {
false
}
}
}
async fn example() {
let mut queue = TaskQueue::new();
// Add different future types - all become BoxFuture
queue.add(async { println!("Task 1") });
queue.add(async {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
println!("Task 2");
});
queue.add(async { println!("Task 3") });
while queue.run_next().await {}
}BoxFuture unifies different future types for storage.
| Type | Send Bound | Use Case |
|------|------------|----------|
| BoxFuture<'a, T> | Yes (included) | Standard boxed futures, collections, traits |
| LocalBoxFuture<'a, T> | No | Non-Send futures, single-threaded contexts |
| Pin<Box<dyn Future<Output = T>>> | No | Explicit typing, non-Send |
| Pin<Box<dyn Future<Output = T> + Send>> | Yes | Manual Send annotation |
| impl Future<Output = T> | Varies | Static dispatch, no allocation |
The difference between BoxFuture and Pin<Box<dyn Future>> is primarily ergonomics, not capability:
Type alias nature: BoxFuture<'a, T> expands to Pin<Box<dyn Future<Output = T> + Send + 'a>>. It's purely a type alias with no runtime difference. The + Send is the key ergonomic addition.
When to use BoxFuture: Use BoxFuture in most cases where you need a boxed, Send future. It's the common case for async Rust—futures that can be sent across threads. Use it for:
When to use Pin<Box>: Use the explicit type when:
!Send future (use LocalBoxFuture or omit Send)Sync in addition to SendWhen to use neither: Use impl Future<Output = T> when:
Key insight: BoxFuture is a convenience alias for the common pattern. Understanding what it expands to helps you know when to use it vs. when to use the explicit Pin<Box<...>> form or impl Future. The choice affects verbosity and clarity, not functionality—pick the form that best communicates intent.