Skip to editor content
learningrust.orglesson 9 of 26

Closures

Closures are anonymous functions that can capture variables from their surrounding scope. If you have used lambdas in Python or arrow functions in JavaScript, closures in Rust fill the same role — but with the added power of Rust's ownership system. The compiler tracks exactly how each captured variable is used, so you get that expressiveness without sacrificing safety — and with optimizations on, the closure inlines away to the same machine code as a direct call.

Closures show up everywhere in idiomatic Rust. They are the glue behind iterator chains like .map() and .filter(), callback-driven APIs, and async runtimes. Understanding closures unlocks the most expressive parts of the language.

Basic Closure Syntax

A closure uses pipes | instead of parentheses for its parameters:

fn main() {
    // A regular function
    fn add_one_fn(x: i32) -> i32 {
        x + 1
    }

    // The same logic as a closure
    let add_one = |x: i32| x + 1;

    // Multi-line closures use braces
    let add_and_print = |x: i32, y: i32| -> i32 {
        let sum = x + y;
        println!("{x} + {y} = {sum}");
        sum
    };

    println!("fn:      {}", add_one_fn(5));
    println!("closure: {}", add_one(5));
    add_and_print(3, 4);
}

Unlike functions, closures can usually infer their parameter and return types from context. You only need type annotations when the compiler cannot figure it out on its own.

Capturing Variables

The real power of closures is that they can reach into the surrounding scope and use variables defined outside their body.

Capture by Reference (Borrowing)

By default, closures borrow captured variables with the least permission needed:

fn main() {
    let greeting = String::from("Hello");

    // This closure borrows `greeting` by shared reference
    let say_hello = || println!("{greeting}");

    say_hello();
    say_hello();

    // `greeting` is still usable here because it was only borrowed
    println!("Original: {greeting}");
}

Capture by Mutable Reference

If a closure modifies a captured variable, it borrows mutably:

fn main() {
    let mut count = 0;

    // Must declare the closure `mut` because it mutates captured state
    let mut increment = || {
        count += 1;
        println!("count = {count}");
    };

    increment();
    increment();
    increment();

    // `count` is accessible again after the closure is no longer used
    println!("final count = {count}");
}

Capture by Value with move

The move keyword forces a closure to take ownership of every variable it captures. This is essential when the closure needs to outlive the scope where it was created, such as when passing it to another thread:

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

    let greet = move || {
        println!("Hello from {name}!");
    };

    greet();

    // This would fail — `name` was moved into the closure:
    // println!("{name}");
}

That commented-out line is not a new rule; it is a rule you already have. Answer this from memory before reading on.

Recall

Without scrolling up: in Ownership & Borrowing you learned that a value is MOVED when it is handed to a new owner, and the original name is unusable afterwards. A move closure captures name by value. Which statement follows from those two facts alone?

To say it plainly: a move closure owns what it captured, exactly as a struct with a String field would. Defining it transfers ownership out of name, so reading name afterwards is the same use-after-move error from Ownership & Borrowing — and the captured String is dropped when the closure is dropped, not when the enclosing function returns. That last point is the whole reason a closure you intend to return, or hand to another thread, must capture by move: the function's locals die at the return, so a borrowing closure would be left pointing at nothing.

The Three Fn Traits

Every closure in Rust implements one or more of these traits. The compiler works out which ones a closure qualifies for from what its body actually does with its captures:

TraitWhat it meansCan call multiple times?
FnBorrows captured values immutablyYes
FnMutMay mutate captured valuesYes
FnOnceConsumes captured values (takes ownership)Only once

The hierarchy is: Fn is a subtrait of FnMut, which is a subtrait of FnOnce. A closure that implements Fn also implements FnMut and FnOnce.

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

    // Fn — only reads `name`
    let greet = || println!("Hi, {name}!");
    call_twice(&greet);

    // FnMut — modifies `total`
    let mut total = 0;
    let mut adder = |x: i32| total += x;
    adder(5);
    adder(10);
    println!("total = {total}");

    // FnOnce — moves `name` out, so it can only run once
    let consume = move || {
        let _owned = name;  // takes ownership inside
        println!("Consumed!");
    };
    consume();
    // consume(); // Would not compile — already consumed
}

fn call_twice(f: &dyn Fn()) {
    f();
    f();
}

Here is the part that catches people out: you never choose which trait a closure implements — the compiler reads the body and decides. move is not that choice. move only settles how variables get into the closure; what the body then does with them is what picks Fn, FnMut or FnOnce. The two closures below are designed to make that split visible: one has move and only reads, the other has no move and consumes. Work out what this prints, and which of them could survive line C being uncommented.

Predict

announce is a move closure whose body only reads. consume has no move at all, but its body hands the captured String away. call_twice demands impl Fn. What does this print, and what would uncommenting line C do?

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

fn main() {
  let tag = String::from("ping");
  let ticket = String::from("T-9");

  // Closure ONE: move, but the body only READS tag.
  let announce = move || println!("{} (len {})", tag, tag.len());

  // Closure TWO: no move, but the body CONSUMES ticket by handing it away.
  let consume = || {
      let owned: String = ticket;
      println!("consumed {}", owned);
  };

  call_twice(announce);   // line A
  consume();              // line B
  // consume();           // line C: commented out
}

It prints ping (len 4) twice, then consumed T-9. announce carries move, so it owns its tag, but its body only reads — so the compiler infers Fn, and call_twice may call it as often as it likes. consume has no move keyword, yet let owned: String = ticket; moves the String out of the closure's environment, which can only happen once — so the compiler infers FnOnce, and uncommenting line C fails with error[E0382]: use of moved value: consume and the note "closure cannot be invoked more than once because it moves the variable ticket out of its environment". The rule to keep: move decides how captures get in; the body decides which trait comes out.

Closures as Function Parameters

Use impl Fn traits to accept closures as arguments. This is how standard library functions like map and filter work:

fn apply_to_5(f: impl Fn(i32) -> i32) -> i32 {
    f(5)
}

fn apply_twice(mut f: impl FnMut(i32) -> i32, value: i32) -> i32 {
    let first = f(value);
    f(first)
}

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

    println!("double(5) = {}", apply_to_5(double));
    println!("square(5) = {}", apply_to_5(square));
    println!("double twice: {}", apply_twice(|x| x * 2, 3));
}

Returning Closures

Functions can return closures using impl Fn as the return type:

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

fn make_multiplier(factor: i32) -> impl Fn(i32) -> i32 {
    move |x| x * factor
}

fn main() {
    let add_10 = make_adder(10);
    let triple = make_multiplier(3);

    println!("add_10(5) = {}", add_10(5));
    println!("triple(7) = {}", triple(7));
}

Note the move keyword: the returned closure must own its captured data because the function's local variables will be dropped when it returns.

A factory like this is also the standard way to build a closure with memory — swap impl Fn for impl FnMut and the returned closure can update its captured state between calls. The catch is that the state belongs to the closure value, not to the factory, so where you call the factory decides what gets remembered. The next program gets that wrong. It compiles without a single warning and prints a perfectly plausible answer, which is what makes it dangerous. Commit a hypothesis before changing anything:

Debug

make_counter hands back a closure that remembers how many times it has been called, and this program uses it to stamp each event with the next sequence number. It compiles cleanly and prints numbers that look like numbers, but they are the wrong ones. Say why before you change anything, then fix it.

fn make_counter() -> impl FnMut() -> u32 {
  let mut count = 0;
  move || {
      count += 1;
      count
  }
}

fn main() {
  // Stamp each event with the next sequence number: 1, 2, 3, 4.
  let events = ["login", "click", "click", "logout"];

  let mut seen = Vec::new();
  for event in &events {
      let mut tally = make_counter();
      seen.push(format!("{}:{}", tally(), event));
  }

  println!("{:?}", seen);

  assert_eq!(
      seen,
      vec!["1:login", "2:click", "3:click", "4:logout"],
      "each event should get the next sequence number, got {:?}",
      seen
  );
  println!("All checks passed.");
}

Expected output: ["1:login", "2:click", "3:click", "4:logout"] All checks passed.

The state belongs to the closure, not to the factory, and each call to make_counter produces a fresh closure with its own count starting at zero. Because make_counter() sits inside the loop body, every iteration built a new counter, called it once, and dropped it — so every event was stamped 1 and the program printed ["1:login", "1:click", "1:click", "1:logout"]. Moving let mut tally = make_counter(); above the loop makes one closure survive all four iterations, and the stamps run 1, 2, 3, 4. The captured state lives exactly as long as the closure value that owns it, which is the move rule again with a counter attached.

Practical Examples

Closures with Iterators

Closures and iterators are a natural pair. Most iterator adapters accept closures:

fn main() {
    let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    // Filter even numbers and double them
    let result: Vec<i32> = numbers
        .iter()
        .filter(|&&x| x % 2 == 0)
        .map(|&x| x * 2)
        .collect();

    println!("Even doubled: {result:?}");

    // Find the first number greater than 5
    let found = numbers.iter().find(|&&x| x > 5);
    println!("First > 5: {found:?}");

    // Sum all numbers using fold
    let sum = numbers.iter().fold(0, |acc, &x| acc + x);
    println!("Sum: {sum}");
}

Event Callbacks

Closures make clean callback APIs:

struct Button {
    label: String,
    on_click: Box<dyn Fn()>,
}

impl Button {
    fn new(label: &str, on_click: impl Fn() + 'static) -> Self {
        Button {
            label: label.to_string(),
            on_click: Box::new(on_click),
        }
    }

    fn click(&self) {
        println!("[{}] clicked!", self.label);
        (self.on_click)();
    }
}

fn main() {
    let save_btn = Button::new("Save", || {
        println!("  -> Saving document...");
    });

    let counter = std::cell::Cell::new(0);
    let count_btn = Button::new("Count", move || {
        counter.set(counter.get() + 1);
        println!("  -> Clicked {} times", counter.get());
    });

    save_btn.click();
    count_btn.click();
    count_btn.click();
}

Interactive Playground

Experiment with closures below. Try modifying the captured variables, changing move semantics, or chaining more iterator methods:

fn main() {
    // Try these experiments:
    // 1. Change the threshold and see how the filter changes
    // 2. Add a .take(3) before .collect() to limit results
    // 3. Create your own closure that captures a variable

    let threshold = 3;
    let data = vec![1, 5, 2, 8, 3, 9, 4, 7];

    let above: Vec<&i32> = data
        .iter()
        .filter(|&&x| x > threshold)
        .collect();

    println!("Above {threshold}: {above:?}");

    // Make a reusable transformer
    let offset = 100;
    let transform = |x: i32| x * 2 + offset;

    for val in &data {
        println!("{val} -> {}", transform(*val));
    }
}

Try It Yourself

Reading about the three Fn traits is not the same as picking the right bound under your own name. This is a build task: a small program that reports its own pass/fail. Three functions are stubbed out — one takes a read-only closure, one takes a closure that updates captured state, and one returns a closure. Run the starter as-is and it fails immediately, naming the first check that did not pass. Implement each until every check passes and it prints All checks passed.

The signatures are already written, and they are the lesson in miniature: impl Fn where the closure only reads, impl FnMut (with mut f) where the caller's closure updates something it captured, and move on the returned closure because the factory's local dies at the return. You write only the bodies.

Build

Finish the build. Three functions are stubbed out and the checks below them fail until each one behaves. Run it as-is to see which check fails first, decide what that function is missing, then implement all three until it prints 'All checks passed.' The checks run top to bottom, so the first failure you see is TODO 1 — implement it first, then work down. Do not change the three signatures: each one's Fn bound is part of the exercise.

// A tiny event-processing kit. Each function takes or returns a closure.

// TODO 1: apply f to every element of values and collect the results.
//   f only READS what it captures, so an Fn bound is enough.
//   transform(&[1, 2], |n| n + 10) -> vec![11, 12]
fn transform(values: &[i32], f: impl Fn(i32) -> i32) -> Vec<i32> {
  let _ = values;
  let _ = f;
  Vec::new()
}

// TODO 2: call f once per element of values, in order. Returns nothing.
//   f is FnMut because the caller's closure will UPDATE captured state.
//   Note the mut f in the signature — a FnMut closure must be called mutably.
fn for_each(values: &[i32], mut f: impl FnMut(i32)) {
  let _ = values;
  let _ = &mut f;
}

// TODO 3: return a closure that adds offset to whatever it is given.
//   The closure outlives this function, so it must OWN offset — hence move.
//   let add5 = adder(5); add5(2) -> 7
fn adder(offset: i32) -> impl Fn(i32) -> i32 {
  let _ = offset;
  move |n| n
}

// --- Build checks: these must all pass. Do not edit below this line. ---
fn main() {
  let readings = [3, 8, 1, 6];

  assert_eq!(transform(&readings, |n| n * 2), vec![6, 16, 2, 12], "transform should map every value through f");

  let mut total = 0;
  for_each(&readings, |n| total += n);
  assert_eq!(total, 18, "for_each should call f once per value, letting it update captured state");

  let bump = adder(10);
  assert_eq!(bump(5), 15, "adder(10) should return a closure that adds 10");
  assert_eq!(bump(0), 10, "the returned closure must be callable more than once");

  println!("All checks passed.");
  println!("Doubled: {:?}", transform(&readings, |n| n * 2));
  println!("Total: {}", total);
  println!("bump(7) = {}", bump(7));
}

Expected output: All checks passed. Doubled: [6, 16, 2, 12] Total: 18 bump(7) = 17

Once it passes, try two variations and predict each before running:

  1. Weaken the bound on for_each. Change its signature to fn for_each(values: &[i32], f: impl Fn(i32)) — dropping both the mut and the Mut. Predict which check breaks before running. None do; it never gets that far. Compilation fails at the call site with error[E0594]: cannot assign to total, as it is a captured variable in a Fn closure, and rustc helpfully points at the parameter with "change this to accept FnMut instead of Fn". The caller's closure did not change — the bound stopped permitting what it always did.
  2. Drop the move from adder. Change its body to |n| n + offset. Decide what the compiler says before running. You get error[E0373]: closure may outlive the current function, but it borrows offset, which is owned by the current function, with the suggestion to add move. This is the returning-closures rule with teeth: the moment a closure escapes the function that built it, borrowing a local is no longer an option.

Practice Exercises

  1. Square closure: Write a closure that takes an i32 and returns its square. Use it with .map() on a vector of numbers.

  2. Custom filter: Create a function filter_above(data: &[i32], min: i32) -> Vec<i32> that uses a closure internally to filter values above min.

  3. Call counter: Write a function that returns a closure implementing FnMut. Each call should return how many times the closure has been invoked so far. (Hint: use move and a mutable variable.)

  4. Compose: Write a function compose that takes two closures f and g and returns a new closure that applies f(g(x)). Test it by composing "double" and "add 1".

Key Takeaways

  • Closures are anonymous functions defined with |params| body syntax
  • They automatically capture variables from their surrounding scope
  • The compiler chooses the least restrictive capture mode: by reference, by mutable reference, or by value
  • The move keyword forces ownership transfer into the closure
  • Three traits define how closures behave: Fn (immutable borrow), FnMut (mutable borrow), and FnOnce (consumes captured values)
  • Use impl Fn(...) to accept or return closures in function signatures

Next Steps

Closures give you flexible, inline functions that capture their environment — and they show up constantly in the combinator methods you're about to meet. In the next lesson, we'll tackle Option and Result, Rust's two essential enums for handling absence and errors. You'll see how methods like map and and_then take closures to transform values safely, without ever reaching for null.

Next lesson

Option and Result

Master Rust Option and Result types for handling absence and errors with combinators, the ? operator, and error propagation

25 min