Syntax Cache
BlogMethodFeaturesHow It WorksBuild a Game
  1. Home
  2. Rust
  3. Rust Closures Practice: Fn, FnMut, FnOnce & Captures
Rust53 exercises

Rust Closures Practice: Fn, FnMut, FnOnce & Captures

Practice Rust closures including capture modes, Fn/FnMut/FnOnce traits, move closures, and functional iterator patterns.

Common ErrorsQuick ReferencePractice
Warm-up1 / 2

Can you write this from memory?

Define a closure `add_one` that takes an i32 and returns it plus 1.

On this page
  1. 1Compiler Error E0373: Closure May Outlive the Current Function
  2. 2Capture Modes: What the Compiler Chooses
  3. 3move Does Not Mean FnOnce
  4. 4Fn vs FnMut vs FnOnce: the Capture Hierarchy
  5. 5Compiler Error E0525: Expected Fn but Closure Implements FnOnce
  6. 6Compiler Error E0596: Cannot Borrow as Mutable
  7. 7Closures with Iterators
  8. 8Function Pointers vs Closures
  9. 9Disjoint Capture (Rust 2021+)
  10. 10Further Reading
Compiler Error E0373: Closure May Outlive the Current FunctionCapture Modes: What the Compiler Choosesmove Does Not Mean FnOnceFn vs FnMut vs FnOnce: the Capture HierarchyCompiler Error E0525: Expected Fn but Closure Implements FnOnceCompiler Error E0596: Cannot Borrow as MutableClosures with IteratorsFunction Pointers vs ClosuresDisjoint Capture (Rust 2021+)Further Reading

Rust closures are anonymous functions that capture their environment. They're everywhere—iterators, callbacks, thread spawning.

The tricky part is understanding how closures capture variables: by reference, mutable reference, or by value. This determines which trait they implement (Fn, FnMut, or FnOnce) and where they can be used.

This page focuses on closure syntax and capture semantics.

Related Rust Topics
Rust Ownership & Borrowing: Move Semantics & Borrow RulesRust Traits & Generics: Trait Bounds, impl Trait, TurbofishRust Collections & Iterators: Vec, HashMap, Iterator Adapters

The most common closure error. std::thread::spawn requires FnOnce + Send + 'static, so the closure must own everything it captures:

// WRONG: closure borrows, but data won't live long enough
// let handle = {
//     let name = String::from("Alice");
//     std::thread::spawn(|| println!("{name}"))  // error[E0373]
// };

// RIGHT: move ownership into the closure
let handle = {
    let name = String::from("Alice");
    std::thread::spawn(move || println!("{name}"))
};
handle.join().unwrap();

The move keyword forces the closure to take ownership of all captured variables, even if a borrow would suffice for the closure body. This satisfies the 'static lifetime bound. If you're coming from Python, closures work quite differently; see Python vs Rust functions compared for a side-by-side breakdown.

Ready to practice?

Start practicing Rust Closures: Fn, FnMut, FnOnce & Captures with spaced repetition

Without move, closures capture by the least restrictive mode needed: immutable borrow → mutable borrow → move. The move keyword overrides this, forcing all captures by value.

Without move, closures capture by the least restrictive mode the body requires:

  1. Immutable borrow (&T) — if the closure only reads the value
  2. Mutable borrow (&mut T) — if the closure mutates the value
  3. Move (T) — if the closure consumes the value (e.g., drops it, returns it, moves it into another binding)

move overrides this: every capture becomes by-value. For Copy types, that means a copy. For owned types, that means a move.

let x = 10;           // Copy type
let s = String::from("hi"); // non-Copy type

let f = move || {
    println!("{x}");  // x is copied in
    println!("{s}");  // s is moved in
};
f();
println!("{x}");      // fine — x was copied
// println!("{s}");   // ERROR: s was moved into the closure

A common misconception. The move keyword controls how values get into the closure. The Fn/FnMut/FnOnce trait depends on what the closure body does with those values:

let name = String::from("Alice");

// move + only reads capture → implements Fn (callable many times)
let greet = move || println!("Hello, {name}");
greet();
greet(); // fine — name is read, not consumed

// move + consumes capture → implements FnOnce (callable once)
let consume = move || {
    drop(name);  // name is consumed
};
consume();
// consume(); // ERROR: cannot call FnOnce again

Fn (reads captures, many calls) ⊂ FnMut (mutates captures, many calls) ⊂ FnOnce (consumes captures, one call). The trait depends on what the body does, not whether you used move.

Every closure implements one or more of these traits. The hierarchy: Fn ⊂ FnMut ⊂ FnOnce.

TraitBody doesCan call
FnReads capturesMany times
FnMutMutates capturesMany times
FnOnceConsumes capturesOnce
// Fn: borrows immutably, callable many times
let x = 10;
let add_x = |n| n + x;
println!("{} {}", add_x(1), add_x(2));

// FnMut: borrows mutably, callable many times
let mut count = 0;
let mut inc = || count += 1;
inc();
inc();

// FnOnce: takes ownership, callable once
let name = String::from("Alice");
let greet = || {
    let owned = name;  // moves name out — consumes it
    println!("Hello, {owned}");
};
greet();
// greet(); // ERROR: cannot call again, name was consumed

This happens when a function expects a reusable closure but your closure consumes a captured value:

// WRONG: closure moves name out, so it's FnOnce
// fn repeat(f: impl Fn()) {
//     f(); f();
// }
// let name = String::from("Alice");
// repeat(|| {
//     let owned = name;  // error[E0525]
//     println!("{owned}");
// });

// RIGHT: borrow instead of consuming
fn repeat(f: impl Fn()) {
    f(); f();
}
let name = String::from("Alice");
repeat(|| {
    println!("{name}");  // borrows — now it's Fn
});

If the closure must consume, change the bound to FnOnce or .clone() before capture.

Both the captured variable and the closure binding need mut:

// WRONG: variable and closure not declared mut
// let count = 0;
// let inc = || count += 1;  // error[E0596]
// inc();                    // error: inc is not mut

// RIGHT: both bindings are mut
let mut count = 0;
let mut inc = || count += 1;
inc();
inc();
println!("{count}"); // 2

Iterator adapters (map, filter, fold) take closures. Use |&x| to destructure references, and chain adapters freely—they're lazy until consumed by collect() or a for loop.

Iterator adapters are where closures shine:

let nums = vec![1, 2, 3, 4, 5];

// map: transform each element
let doubled: Vec<i32> = nums.iter().map(|&x| x * 2).collect();

// filter: keep elements matching a predicate
let evens: Vec<&i32> = nums.iter().filter(|&&x| x % 2 == 0).collect();

// fold: accumulate into a single value
let sum = nums.iter().fold(0, |acc, x| acc + x);

// chaining: filter then map
let big_doubled: Vec<i32> = nums.iter()
    .filter(|&&x| x > 2)
    .map(|&x| x * 2)
    .collect();

Closures that capture nothing can coerce to function pointers (fn). Closures that capture cannot:

// Works: no captures, coerces to fn
let add: fn(i32, i32) -> i32 = |a, b| a + b;

// ERROR: captures x, cannot coerce to fn
// let x = 10;
// let add_x: fn(i32) -> i32 = |n| n + x;

// RIGHT: use impl Fn or generics instead
fn apply(f: impl Fn(i32) -> i32, n: i32) -> i32 { f(n) }
let x = 10;
let result = apply(|n| n + x, 5); // 15

If an API requires fn(), make sure your closure captures nothing. If it must capture, the API needs to accept impl Fn() or Box<dyn Fn()>. The traits and generics guide covers how Fn, FnMut, and FnOnce work as trait bounds in generic functions.

Since edition 2021, closures capture individual fields instead of the whole struct:

struct Config { name: String, retries: u32 }
let config = Config { name: "app".into(), retries: 3 };

// Rust 2021: captures only config.retries, not all of config
let get_retries = || config.retries;

// config.name is still usable here
println!("{}", config.name);

In edition 2018, this would fail because the closure captured all of config. If you need the old behavior, use move or capture a reference to the whole struct explicitly. Closures accepting trait-bounded behaviors enable the Strategy design pattern -- select an algorithm at runtime by passing different closures.

  • The Rust Book: Closures — capture modes, Fn traits, iterator patterns
  • Rust Reference: Closure Types — precise capture rules
  • Rust Edition Guide: Disjoint Capture — 2021 field-level captures
  • Advanced Functions & Closures — function pointers, returning closures

When to Use Rust Closures: Fn, FnMut, FnOnce & Captures

  • Use closures with iterator adapters (map, filter, fold).
  • Use `move` closures when spawning threads or returning closures.
  • Use `Fn` bounds when you need to call a closure multiple times.
  • Use `FnOnce` bounds when the closure only needs to run once.
  • Prefer closures over named functions for short, localized logic.

Check Your Understanding: Rust Closures: Fn, FnMut, FnOnce & Captures

Prompt

What is the difference between Fn, FnMut, and FnOnce?

What a strong answer looks like

FnOnce can be called once (consumes captured values). FnMut can be called multiple times but may mutate captures. Fn can be called multiple times without mutation. Every Fn is FnMut, every FnMut is FnOnce.

What You'll Practice: Rust Closures: Fn, FnMut, FnOnce & Captures

Write basic closures with |args| syntaxUnderstand automatic capture modesUse move closures for ownership transferApply closures with map, filter, foldUnderstand Fn, FnMut, FnOnce traitsReturn closures from functions with impl FnStore closures in structs with generics or Box<dyn Fn>

Common Rust Closures: Fn, FnMut, FnOnce & Captures Pitfalls

  • You get `closure may outlive the current function` when spawning a thread → the closure borrows a local variable that will be dropped when the function returns → add `move` before the closure: `std::thread::spawn(move || ...)`.
  • You get `cannot borrow as mutable, as it is not declared as mutable` → the closure mutates a captured variable but the bindings are not `mut` → declare both the variable and the closure as mutable: `let mut count = 0; let mut inc = || count += 1;`.
  • You get `expected Fn, but this closure only implements FnOnce` → the closure consumes a captured value so it can only run once → borrow instead of moving, or `.clone()` the value before capture.
  • `move` forces ownership transfer of everything the closure mentions, which can force cloning later or prevent reuse → prefer borrowing with `&vec` when the closure doesn't need to own the data, or capture an `Arc` when multiple owners are needed.
  • You pass a closure where a function pointer is expected and get a type error → closures that capture variables cannot coerce to `fn()` → use a closure that captures nothing, or change the signature to accept `impl Fn()` instead.

Rust Closures: Fn, FnMut, FnOnce & Captures FAQ

When do I need the move keyword?

Use `move` when the closure needs to own its captured variables—typically when returning a closure, spawning threads, or when the closure outlives the current scope.

How does Rust decide how to capture variables?

Rust infers the least restrictive capture mode needed: immutable borrow if possible, mutable borrow if mutation is needed, ownership if the value is consumed. Use `move` to force ownership.

Can closures have explicit type annotations?

Yes: `|x: i32| -> i32 { x + 1 }`. Usually unnecessary since types are inferred from context, but sometimes needed for clarity or when inference fails.

What is the type of a closure?

Each closure has a unique, anonymous type. You cannot name it directly. Use `impl Fn(...)` for return types or generics with `Fn` bounds for parameters.

Why use closures instead of functions?

Closures capture their environment. Functions cannot. Use closures when you need access to surrounding variables. Use functions when you want reusable, named logic.

Rust Closures: Fn, FnMut, FnOnce & Captures Syntax Quick Reference

Basic closure
let add = |a, b| a + b;
With body
let f = |x| {
    let y = x + 1;
    y * 2
};
Type annotations
let f = |x: i32| -> i32 { x + 1 };
Capture by ref
let x = 5;
let f = || x + 1;
Move closure
let s = String::from("hi");
let f = move || println!("{}", s);
Mutable closure
let mut count = 0;
let mut inc = || count += 1;
With map
nums.iter().map(|x| x * 2)
With filter
nums.iter().filter(|x| *x > 0)
Fn parameter
fn apply<F: Fn(i32) -> i32>(f: F, x: i32) -> i32

Rust Closures: Fn, FnMut, FnOnce & Captures Sample Exercises

Example 1Difficulty: 1/5

Fill in how to call the closure with 5.

double
Example 2Difficulty: 1/5

Fill in the closure syntax for a closure with no parameters.

||
Example 3Difficulty: 2/5

Fill in the return expression for a closure that doubles `x` and adds 1.

doubled + 1

+ 50 more exercises

Further Reading

  • Rust Newtype Pattern: Catch Unit Bugs at Compile Time18 min read

Related Design Patterns

Strategy Pattern

Start practicing Rust Closures: Fn, FnMut, FnOnce & Captures

Free daily exercises with spaced repetition. No credit card required.

← Back to Rust Syntax Practice
Syntax Cache

Build syntax muscle memory with spaced repetition.

Product

  • Pricing
  • Our Method
  • Daily Practice
  • Design Patterns
  • Interview Prep

Resources

  • Blog
  • Compare
  • Cheat Sheets
  • Vibe Coding
  • Muscle Memory

Languages

  • Python
  • JavaScript
  • TypeScript
  • Rust
  • SQL
  • GDScript

Legal

  • Terms
  • Privacy
  • Contact

© 2026 Syntax Cache

Cancel anytime in 2 clicks. Keep access until the end of your billing period.

No refunds for partial billing periods.