Article · 1 min read

17 — Closures

Closures are anonymous functions that capture their environment. They bridge the gap between functions and objects.

Syntax

let add = |a, b| a + b;
let square = |x: i32| x * x;
let greet = |name: &str| format!("hi {name}");
let block = |x| { let y = x + 1; y * 2 };
let no_args = || 42;
let void_closure = || { /* do something */ };
  • Parameter types often inferred (especially when called immediately).
  • Single expression OR { } block.
  • Can't have generic parameters (no |<T> x| ...).

Capturing the Environment

Closures capture variables from the enclosing scope. The capture mode determines how:

let n = 5;
let add_n = |x| x + n;            // borrows n by reference
let n_mut = String::from("a");
let consume = move || n_mut;       // moves n_mut into closure

Capture Modes (Trait Hierarchy)

TraitHow it captures
FnOnceMay consume captures (move out) — callable once
FnMutMay mutate captures — callable multiple times (mutably)
FnOnly immutable borrows — callable any number of times

Each is a supertrait of the next: Fn: FnMut: FnOnce. So every Fn is also FnMut and FnOnce.

let mut s = String::from("hi");
let push_closure = || s.push('!');       // FnMut — needs mut capture
let consume_closure = || drop(s);         // FnOnce — consumes s

move Keyword

Forces capture by value (move) regardless of how the closure uses them:

let s = String::from("hi");
let f = move || println!("{s}");   // s moved into f; original invalid

move is essential for thread spawning — closures sent to other threads must own their captures ('static).

Disjoint Closure Captures (Edition 2021)

let a = String::from("a");
let b = String::from("b");
let f = || println!("{a} {b}");   // edition 2018: borrows BOTH a and b
                                   // edition 2021: borrows only what's used in each branch

The 2021 edition captures only the used fields of disjoint structs, enabling more code to compile.

Closures as Arguments

fn apply<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 { f(x) }
fn apply_mut<F: FnMut(i32) -> i32>(mut f: F, x: i32) -> i32 { f(x) }
fn apply_once<F: FnOnce() -> i32>(f: F) -> i32 { f() }

apply(|x| x + 1, 5);

Use impl Fn(...) for shorthand:

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

Returning Closures

fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
    move |x| x + n
}

let add5 = make_adder(5);
add5(10);   // 15

Use move so the closure owns its captures (otherwise it'd borrow a stack variable that's gone).

For multiple closure types in different branches, use Box<dyn Fn()>:

fn make(pred: bool) -> Box<dyn Fn(i32) -> i32> {
    if pred { Box::new(move |x| x + 1) }
    else    { Box::new(move |x| x - 1) }
}

Closure Type is Unique

Each closure has an anonymous, unnameable type generated by the compiler. Two structurally identical closures have different types. This is why Box<dyn Fn()> exists for heterogeneous collections.

Closures Implement Traits

impl Fn(i32) -> i32 for SomeClosureType { ... }

Function pointers fn(A) -> B also implement Fn, so you can pass plain functions where impl Fn is expected.

Fn/FnMut/FnOnce Object Safety

let f: Box<dyn Fn(i32) -> i32> = Box::new(|x| x + 1);
let fm: Box<dyn FnMut(i32) -> i32> = Box::new(|x| x + 1);
let fo: Box<dyn FnOnce(i32) -> i32> = Box::new(|x| x + 1);

Box<dyn FnOnce> is callable since Rust 1.35 (special support).

Closures and Iterators

Closures are the currency of iterator adapters:

v.iter().map(|x| x * 2).filter(|x| *x > 5).for_each(|x| println!("{x}"));

? in Closures

let parse = |s: &str| s.parse::<i32>()?;   // closure can use ?

Closures can use ? if their return type supports it (Result or Option).

Recursion in Closures

Closures can't easily recurse — they don't have a name. Workarounds:

  • Use a regular fn.
  • Use a Box<dyn Fn> and pass it to itself.
  • Use Y combinator (academic).

move Closure Captures and Lifetime

let s = String::from("hi");
let f = move || println!("{s}");
// s moved into f; f now owns the String

Without move, f would borrow s, which means s must outlive f. With move, f owns its captures and can be 'static.

Capture By Reference, By Mut Reference, By Value

let n = 5;
let f1 = || println!("{n}");        // &n
let mut m = 0;
let mut f2 = || m += 1;             // &mut m
let s = String::from("x");
let f3 = || drop(s);                 // moves s out — FnOnce
let f4 = move || println!("{n}");    // copies n (i32 is Copy)

The compiler picks the least restrictive capture mode by default. move forces everything to move (or copy if Copy).

Higher-Order Closures

fn make<F: Fn(i32) -> i32>(f: F) -> impl Fn(i32) -> i32 {
    move |x| f(x) + 1
}

Functions returning closures returning closures — typical in functional pipelines.

partial / Currying

Rust doesn't have built-in currying, but move closures make it easy:

let add = |a: i32| move |b: i32| a + b;
let add5 = add(5);
add5(3);   // 8

Edge Cases & Pitfalls

  • Capture lifetime: a closure borrowing from local vars can't escape the local's scope. Use move (often with 'static requirement).
  • FnOnce and Vec::map: map consumes the iterator but only requires FnMut; if you consume captures inside, you might need a different signature.
  • Fn vs FnMut vs FnOnce matching: passing a FnOnce closure to a function expecting Fn won't compile. Pass Fn when possible.
  • Recursive closure: not directly possible; use a fn instead.
  • Closures with &mut self: a method that takes a closure that also borrows self mutably conflicts. Restructure (e.g., extract a value first).
  • Capturing by RefCell: if you need to mutate through a closure called multiple times behind an &-reference, use RefCell for interior mutability.
  • move doesn't always move: move || println!("{x}") for x: i32 copies; move only forces by-value capture (which is Copy-duplicating for Copy types).
  • move || in threads: required for std::thread::spawn since the closure must be 'static + Send.

thread::spawn and 'static + Send

let data = vec![1, 2, 3];
std::thread::spawn(move || {
    println!("{:?}", data);   // OK — data moved in
});

Without move, you'd borrow data, which doesn't satisfy 'static. move + 'static + Send is the recipe for thread closures.

Summary

Closures capture by ref (Fn), mut ref (FnMut), or value (FnOnce). move forces by-value. Use impl Fn(...)/FnMut/FnOnce for generic APIs. Closures are essential for iterator combinators and async. Returning closures requires impl Fn (single type) or Box<dyn Fn> (heterogeneous).

Next: Error handling — Result, Option, and ?.