Updated Aug 5, 2026

19 - Closures

📋 Jump to Takeaways

🎁 You write a tiny anonymous function, and it just grabs variables from the surrounding code without you passing them in. No special syntax to "close over" the environment, no explicit capture lists. The compiler figures out what to borrow, what to mutate, and what to move, all from how you use the variables inside.

Closure Syntax

A closure is an anonymous function you can store in a variable or pass as an argument. You write parameters between pipes instead of parentheses.

fn main() {
    let add_one = |x| x + 1;
    let multiply = |x, y| x * y;

    println!("{}", add_one(5));       // 6
    println!("{}", multiply(3, 4));   // 12

    // Multi-line closures use braces
    let calculate = |x: i32| {
        let doubled = x * 2;
        doubled + 10
    };
    println!("{}", calculate(5));     // 20
}

Regular functions require explicit type annotations — closures don't. The compiler infers parameter and return types from how the closure is used:

fn double_fn(x: i32) -> i32 { x * 2 }  // function: must annotate

let double_cl = |x| x * 2;
double_cl(5);  // closure: compiler sees i32, infers the rest

Once inferred, the types are locked in. You can't call the same closure with different types:

let identity = |x| x;
identity(5);       // locks in x: i32
identity("hello"); // ❌ ERROR — already inferred as i32

Closures are not generic. If you need something that works with multiple types, use a generic function instead:

fn identity<T>(x: T) -> T { x }

println!("{}", identity(5));       // ✅ i32
println!("{}", identity("hello")); // ✅ &str

Capturing Variables

Closures can capture variables from their surrounding scope. This is what makes them more powerful than plain functions.

fn main() {
    let name = String::from("Rust");
    let greeting = || println!("Hello, {}!", name); // borrows name
    greeting(); // Hello, Rust!
    println!("{}", name); // still valid — closure only borrowed it
}

Rust determines how a closure captures based on what the closure body does. This maps to three traits:

  • Fn borrows captured values immutably. Can be called many times.
  • FnMut borrows captured values mutably. Can be called many times but needs mut access.
  • FnOnce takes ownership of captured values. Can only be called once.

Rust picks the least restrictive trait automatically. If the closure only reads a variable, it's Fn. If it mutates, it's FnMut. If it moves the value out, it's FnOnce.

fn main() {
    let name = String::from("Alice");

    // Fn — only reads `name`, borrows it immutably
    let greet = || println!("Hello, {}", name);
    greet();
    greet(); // ✅ can call multiple times
    println!("{}", name); // ✅ `name` still usable

    let mut count = 0;

    // FnMut — modifies `count`, borrows it mutably
    let mut increment = || { count += 1; };
    increment();
    increment(); // ✅ can call multiple times
    println!("count: {}", count); // count: 2

    let message = String::from("goodbye");

    // FnOnce — moves `message` into the closure body
    let consume = || {
        let m = message;
        println!("{}", m);
    };
    consume(); // goodbye
    // consume(); // ❌ ERROR: can't call again, `message` was moved
}

The move Keyword

Sometimes you need the closure to own the captured data, especially when sending it to another thread. Use move before the pipes.

fn main() {
    let data = vec![1, 2, 3];

    let owns_data = move || {
        println!("{:?}", data);
    };

    owns_data(); // [1, 2, 3]
    // println!("{:?}", data); // ❌ ERROR: data was moved into the closure
}

With move, all captured variables are moved into the closure regardless of how the body uses them.

Functions That Accept Functions

In Go you pass a function by writing the signature inline: func apply(f func(int) int, x int) int. In Rust you use the Fn traits as bounds.

fn apply(f: impl Fn(i32) -> i32, x: i32) -> i32 {
    f(x)
}

fn main() {
    let double = |n| n * 2;
    let square = |n| n * n;

    println!("{}", apply(double, 5)); // 10
    println!("{}", apply(square, 5)); // 25
}

impl Fn(i32) -> i32 means "anything callable that takes an i32 and returns an i32". This works for closures and regular functions.

You can also pass a named function directly:

fn add_one(n: i32) -> i32 {
    n + 1
}

fn apply_twice(f: impl Fn(i32) -> i32, x: i32) -> i32 {
    f(f(x))
}

fn main() {
    println!("{}", apply_twice(add_one, 5)); // 7
}

Which Fn trait to use as the bound depends on what the caller might pass:

Bound What the function can do with it Use when
impl Fn() Call it many times, read-only Most cases
impl FnMut() Call it many times, might mutate Accumulating results
impl FnOnce() Call it exactly once Consuming a value

Pick the most permissive bound that works. FnOnce accepts all closures. Fn is the most restrictive (but most callers prefer it because they can call repeatedly).

fn run_twice(f: impl Fn()) {
    f();
    f();
}

fn run_once(f: impl FnOnce()) {
    f();
    // f(); // ❌ ERROR: can't call FnOnce twice
}

fn main() {
    let greeting = String::from("hello");
    run_twice(|| println!("{}", greeting)); // ✅ Fn, borrows greeting

    let name = String::from("world");
    run_once(move || println!("{}", name)); // ✅ FnOnce, moves name
}

There's also the bare fn pointer type for when you don't need closures at all:

fn apply_fn_ptr(f: fn(i32) -> i32, x: i32) -> i32 {
    f(x)
}

fn main() {
    println!("{}", apply_fn_ptr(|n| n + 1, 5)); // 6
}

fn(i32) -> i32 (lowercase) is a function pointer. It works for closures that capture nothing and for named functions. It cannot accept closures that capture variables. Use impl Fn when you want to accept closures too.

Key Takeaways

  • Closures use |params| body syntax and infer types from context
  • Fn borrows immutably, FnMut borrows mutably, FnOnce consumes captured values
  • move forces a closure to take ownership of all captured variables
  • Accept closures as parameters with impl Fn(Args) -> Return
  • fn(Args) -> Return (lowercase) is for function pointers only, no captures allowed
  • Fn is the most restrictive trait (read-only); FnOnce is the most permissive (accepts everything)
  • Once a closure's types are inferred, they're locked in for all subsequent calls — use a generic function if you need multiple types

🎁 You've seen closures passed to functions. But what about chaining them? Write .map().filter().collect() and it reads like English, yet compiles to the same machine code as a hand-written loop. That's the iterator system, and it changes how you think about data processing.

📝 Ready to test your knowledge?

Answer the quiz below to mark this lesson complete.

Spot something off? Report an issue
© 2026 ByteLearn.dev. Free courses for developers. · Privacy