Recommended resources
The freely available resources this lesson will direct you to.
Good to see you again. The previous lesson established a central rule for borrowed parser data: a value such as BoxView<'src> can remain valid only while its source buffer remains alive. This lesson changes the question from “what does this value borrow from?” to “what data does this callable object own or borrow from its surroundings?”
Closures are essential to the player architecture ahead. They can serve as callbacks for state changes, and the same capture rules determine whether an asynchronous task can safely outlive the function that created it. By the end of this lesson, you should be able to choose move deliberately, identify what is no longer usable after a closure is created, and explain why async move is the usual starting point for independently running tasks.
Closures carry behavior and an environment
A function item has a fixed name and receives all its data through parameters. A closure is an anonymous callable value that can additionally capture values from its surrounding scope.
let target_buffer_seconds = 8.0;
let is_buffered_enough = |current_buffer_seconds: f64| {
current_buffer_seconds >= target_buffer_seconds
};
assert!(is_buffered_enough(10.5));
The closure receives current_buffer_seconds as an explicit parameter, but it obtains target_buffer_seconds from the enclosing scope. Conceptually, the closure value contains both its code and some captured environment.
Rust selects the least-permissive capture mode that the closure body requires:
| Closure body use | Typical capture | Effect on outer value |
|---|---|---|
| Reads a value | Immutable borrow | Outer value remains usable |
| Mutates a value | Mutable borrow | Outer value is mutably borrowed while needed |
| Takes a value | Ownership | Outer value is moved |
Uses move |
Ownership of used captures | Outer value is moved, unless it is Copy |
Read the Rust Book’s two closure subsections now. They establish both the capture modes and the callable traits that callback APIs use.
Closures - The Rust Programming Language
Read the Rust Book’s discussion of closures, especially its distinction between borrowing a captured value and forcing an owned capture with move. Then connect that capture decision to the Fn, FnMut, and FnOnce bounds used by callback-taking APIs.
In the subsection “Capturing References or Moving Ownership,” read the full discussion and Listings 13-4 through 13-6. Focus on the capture modes: Rust normally infers the weakest capture it can, while move deliberately changes the closure’s capture to ownership. Then read the full subsection “Moving Captured Values Out of Closures.” Pay particular attention to the Fn trait overview and the sort_by_key example. Distinguish moving a value into a closure from moving a captured value out of the closure when it runs.
A capture by immutable reference is convenient when the closure stays within the scope of the borrowed value:
let codec = String::from("avc1");
let describe = || {
println!("selected video codec: {codec}");
};
describe();
println!("still available: {codec}");
describe only reads codec, so Rust can capture &codec. The original String remains owned by the enclosing scope.
A mutation changes the capture requirement:
let mut decoded_frames = 0;
let mut record_frame = || {
decoded_frames += 1;
};
record_frame();
record_frame();
assert_eq!(decoded_frames, 2);
Here the closure needs mutable access to decoded_frames. While that mutable borrow is live, Rust prevents competing access that could violate exclusive mutation.
These inferred borrows are excellent for short-lived local work. They are inappropriate when some other component will retain the callback and invoke it later. The enclosing stack frame may be gone by then.
move: put the captured data inside the closure
Prefixing a closure with move tells Rust to capture every value used from the environment by value. For non-Copy data, that transfers ownership from the outer scope into the closure object.
let representation_id = String::from("video-1080p");
let report_selection = move || {
println!("selected representation: {representation_id}");
};
report_selection();
// println!("{representation_id}"); // Error: ownership moved into the closure.
The closure now owns the String. This is not a copy: after report_selection is created, the outer binding can no longer be used.
For a Copy type, such as an integer or boolean, move copies the value into the closure:
let retry_limit: u8 = 3;
let policy = move || {
println!("maximum retries: {retry_limit}");
};
println!("outer retry limit: {retry_limit}");
policy();
The same syntax therefore has different ownership consequences depending on the captured type. When in doubt, treat a move closure as an ownership boundary and let the compiler show whether a capture is copied or moved.
A move closure is not necessarily FnOnce
A frequent misunderstanding is that move means “this closure can run only once.” It does not.
move determines how values enter the closure. The closure body determines whether those captured values are moved out of it during a call.
This closure owns stream_label, but only reads it, so it can be called repeatedly:
let stream_label = String::from("audio");
let report_progress = move |downloaded_segments: usize| {
println!("{stream_label}: downloaded {downloaded_segments} segments");
};
report_progress(1);
report_progress(2);
It can implement Fn, the most flexible closure trait. The usual relationship is:
| Trait bound required by API | What the API may do |
|---|---|
FnOnce |
Call the closure at most once |
FnMut |
Call it repeatedly, with mutable access to its captured state |
Fn |
Call it repeatedly without mutating or consuming its captured state |
Every closure implements FnOnce, because any closure can be called once. A closure that implements Fn is also acceptable where FnMut or FnOnce is required; it makes fewer demands on its environment.
Now compare a closure that consumes its capture when called:
let initialization_data = String::from("codec configuration");
let consume_once = move || {
let data_for_decoder = initialization_data;
println!("passing {data_for_decoder} to decoder setup");
};
consume_once();
// consume_once(); // Error: `initialization_data` was moved out on the first call.
initialization_data enters the closure at creation time because of move. Then the assignment to data_for_decoder moves it out when the closure runs. Nothing remains for a second invocation, so this closure implements only FnOnce.
That distinction lets an API accurately declare its callback contract. A fallback computation that is needed at most once can accept F: FnOnce() -> T. A persistent player-state observer normally needs an Fn or FnMut callback because it may be invoked many times.
Owned callbacks can safely outlive their creator
Suppose a component stores a listener to report player events later:
#[derive(Debug)]
enum PlayerEvent {
PlaybackStarted,
PlaybackEnded,
}
fn store_listener<F>(_listener: F)
where
F: Fn(PlayerEvent) + Send + 'static,
{
// A real player component would retain the listener here.
}
The important bounds are not decoration:
Fn(PlayerEvent)says the listener may be invoked repeatedly without consuming its environment.Sendsays the callback can be transferred to another thread if the implementation needs that.'staticsays the callback cannot contain ordinary references to short-lived stack values.
An owned capture satisfies the last condition naturally:
let track_label = String::from("English audio");
store_listener(move |event| {
println!("{track_label}: {event:?}");
});
The listener owns track_label. If the component retains the callback after this function returns, the String remains alive as part of the callback object.
Crucially, a bound of 'static does not mean the callback must live forever, and it does not require intentionally leaking memory. It means the callback does not borrow data that might disappear before the callback is dropped. Owned values meet that requirement because the closure owns and eventually drops them.
By contrast, a borrowed capture usually cannot meet this requirement:
let track_label = String::from("English audio");
store_listener(|event| {
println!("{track_label}: {event:?}");
});
Without move, this closure borrows track_label. A listener stored beyond the end of the current function could hold a dangling reference, so Rust rejects it when 'static is required.
The design choice is meaningful:
- If a callback is called immediately and never retained, borrowing is often appropriate.
- If another component may store the callback, use owned captures, typically through
move. - If several components genuinely need the same state, do not try to keep borrowing it indefinitely. Later, you will use owned shared handles and message passing to make that lifetime explicit.
From move closures to async move blocks
An async block also captures variables from its surrounding scope. An ordinary async block may borrow those variables; an async move block owns its captures. The latter form is essential when the future must survive beyond its creator’s scope.
Consider an asynchronous helper:
async fn announce(label: &str) {
println!("starting download for {label}");
}
This function cannot safely return a future that borrows a local String:
use std::future::Future;
fn bad_future() -> impl Future<Output = ()> {
let label = String::from("video-720p");
async {
announce(&label).await;
}
}
The future returned by bad_future would contain a reference to label, but label is dropped when bad_future returns. Rust prevents that dangling reference.
Make the future own the String instead:
use std::future::Future;
fn good_future() -> impl Future<Output = ()> {
let label = String::from("video-720p");
async move {
announce(&label).await;
}
}
The String becomes part of the future’s stored state. Inside the block, announce still receives a borrowed &str, but that borrow points into label, which is owned by the future and remains present until the future is completed or dropped.
Read the async book’s explanation now. It provides the compact comparison between an ordinary async block and an async move block.
async/await - Asynchronous Programming in Rust
Read the async book’s discussion of futures that borrow local data and the async move solution. This is the bridge between ordinary owned closures and the task code you will write in the next module.
Read the opening discussion in this section, including both bad and good, to see why a future returned from a function cannot borrow one of that function’s local values. Locate the paragraph ending in the borrowing constraint. Then read the entire subsection “async move.” Focus on the ownership tradeoff: an async move block gives up access to moved values outside the future, but makes the future self-contained with respect to those values.
A useful rule is:
If a future is awaited entirely before the enclosing scope ends, borrowing may be fine. If the future will be returned, stored, or handed to an independent task, begin by considering
async move.
This does not mean every value must be moved. A future can borrow safely when its owner guarantees the source outlives it. But the ownership model is generally easier to reason about at task boundaries.
Why spawned tasks normally begin with async move
A task submitted to tokio::spawn may continue after the function that spawned it has returned. Its future must therefore not borrow stack locals from that function.
A typical task shape looks like this:
let segment_name = String::from("video-00042.m4s");
let handle = tokio::spawn(async move {
announce(&segment_name).await;
});
segment_name is moved into the task future. The task remains valid even if the surrounding function returns immediately afterward.
The following superficially similar code fails:
let segment_name = String::from("video-00042.m4s");
let name_ref = &segment_name;
tokio::spawn(async move {
announce(name_ref).await;
});
async move moves name_ref into the future, but name_ref is still only a reference to segment_name. Moving a reference does not turn its referent into owned data. The task would retain a pointer to a local String that may be dropped, so it cannot satisfy the task’s 'static requirement.
Watch this short explanation of the requirement before the course moves fully into futures and Tokio.
Why Rust’s tokio spawn Needs 'static
Watch “Why Rust’s tokio spawn Needs 'static” from Semicolon. It explains the lifetime problem that appears when a spawned task borrows a local variable, then shows why transferring ownership solves the basic case.
Watch the lifetime problem to connect task independence with Tokio’s 'static requirement. Then watch the move fix and compare it with the good_future example above. Notice that the fix eliminates a borrow; it does not somehow extend the lifetime of a borrowed local.
async move handles the ownership side of task construction. A multithreaded runtime adds one more requirement: captured values that may remain alive across an .await generally need to be Send, because the runtime may run the task on different threads. You will examine that requirement in detail when you begin spawning Tokio tasks; for now, recognize the separate questions:
- Does the future own its necessary data? This is often addressed by
async move. - Can that owned data safely move between threads? This is the role of
Send.
Key takeaways
- A closure can capture surrounding values by immutable borrow, mutable borrow, or ownership.
moveforces ownership capture of values used by a closure. Non-Copycaptured values are no longer usable through their original outer bindings.movedoes not by itself make a closureFnOnce. A closure becomesFnOnce-only when its body consumes a captured value during a call.- Callback bounds express an invocation contract:
FnOncefor at most one call,FnMutfor repeated mutation, andFnfor repeated read-only calls. - A closure or future satisfying
'staticneed not live forever; it must simply contain no non-static borrowed data. async movetransfers referenced outer values into a future, allowing that future to outlive the creating scope.- Moving a reference merely moves the reference. To make a task independent, move or otherwise provide ownership of the underlying data.
You have now completed the Rust foundations module. Next, the course turns to the mechanics underneath async syntax: why calling an async fn creates a lazy Future rather than immediately executing its body.