Closures

Reviewed & published by Brayan K

Rust is a systems programming language focused on speed, memory safety, and fearless concurrency — and closures are anonymous functions that can capture variables from the scope around them.

Part of the free Rust course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

In this lesson you'll write closures, capture the environment, pass and return them with the Fn traits, and use move.

What You'll Learn in This Lesson

1️⃣ Closure Basics and Capturing

A closure is written with its parameters between vertical bars, like |a, b| a + b. Types are usually inferred. The defining feature is that a closure captures variables from the surrounding scope — so it can use values that aren't passed in as parameters.

fn main() {
    // A closure is an anonymous function. Parameters go between | bars |.
    let add = |a: i32, b: i32| a + b;
    println!("add: {}", add(2, 3));   // -> add: 5

    // Types are usually inferred, so you can often omit them.
    let double = |x| x * 2;
    println!("double: {}", double(10));   // -> double: 20

    // The big difference from fn: closures CAPTURE their environment.
    let factor = 3;
    let scale = |x| x * factor;   // captures factor from the surrounding scope
    println!("scale: {}", scale(5));   // -> scale: 15

    // A multi-line closure uses braces.
    let describe = |n: i32| {
        let kind = if n % 2 == 0 { "even" } else { "odd" };
        format!("{} is {}", n, kind)
    };
    println!("{}", describe(7));   // -> 7 is odd
}

The scale closure used factor even though it was never a parameter — it captured it from the enclosing scope. A plain fn can't do that.

2️⃣ Passing, Returning, and the Fn Traits

Closures are values you can pass to and return from functions. A parameter typed impl Fn(i32) -> i32 accepts any read-only closure of that shape. To return a closure that outlives the function, use move so it owns its captures. Mutating closures use FnMut.

// Closures are commonly passed to other functions. A parameter typed
// "impl Fn(i32) -> i32" accepts any closure taking an i32 and returning one.
fn apply(f: impl Fn(i32) -> i32, value: i32) -> i32 {
    f(value)
}

// A function can also RETURN a closure. "move" makes the closure take
// ownership of captured variables so it can outlive this function.
fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
    move |x| x + n
}

fn main() {
    let squared = apply(|x| x * x, 5);
    println!("squared: {}", squared);   // -> squared: 25

    let add_ten = make_adder(10);
    println!("add_ten(7): {}", add_ten(7));   // -> add_ten(7): 17

    // FnMut closures can mutate captured state across calls.
    let mut count = 0;
    let mut increment = || { count += 1; count };
    println!("{} {} {}", increment(), increment(), increment());  // -> 1 2 3
}

make_adder returned a closure that captured n by value (thanks to move), so it stays valid after the function returns. The increment closure mutated count across calls — that's FnMut.

Your turn. Fill in the blanks marked ___, then run it.

// 🎯 YOUR TURN — finish the closures, then run it.
fn main() {
    // 1) A closure that triples its input. Fill in the body.
    let triple = |x: i32| x ___ 3;    // 👉 the operator to multiply
    println!("triple(4) = {}", triple(4));   // expected: triple(4) = 12

    // 2) A closure that captures bonus from the surrounding scope.
    let bonus = 100;
    let with_bonus = |score| score + ___;    // 👉 the captured variable
    println!("with_bonus(50) = {}", with_bonus(50));  // expected: 150
}

// ✅ Expected output:
//    triple(4) = 12
//    with_bonus(50) = 150

Mini-Challenge: A Closure Factory

Write a function that returns a multiplying closure with move. Run it with cargo run.

// 🎯 MINI-CHALLENGE: a closure factory.
//
// 1. Write fn multiplier(factor: i32) -> impl Fn(i32) -> i32 that returns a
//    closure multiplying its argument by factor. Use "move" so the closure
//    owns factor.
// 2. In main, create a "double" closure with multiplier(2) and a "triple"
//    closure with multiplier(3).
// 3. Print double(5) and triple(5).
//
// ✅ Example output:
//    double(5) = 10
//    triple(5) = 15

fn main() {
    // your code here
}

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — Closures

GoalRust Syntax
Closurelet f = |x| x + 1;
Multi-line|x| { let y = x; y }
Take a closuref: impl Fn(i32) -> i32
Return a closure-> impl Fn(i32) -> i32
Own capturesmove |x| x + n
Mutatinglet mut f = || count += 1;

🎉 Lesson Complete!

Practice quiz

What defining ability does a closure have that a plain fn does not?

  • It can be recursive
  • It can capture variables from the surrounding scope
  • It runs faster
  • It returns multiple values

Answer: It can capture variables from the surrounding scope. Closures capture variables from their environment; plain functions only see parameters and globals.

How are a closure's parameters written?

  • In parentheses ( )
  • Between vertical bars | |
  • In brackets [ ]
  • After the fn keyword

Answer: Between vertical bars | |. Closure parameters go between vertical bars, e.g. |a, b| a + b.

What does this print? let factor = 3; let scale = |x| x * factor; println!("{}", scale(5));

  • 3
  • 5
  • 15
  • 8

Answer: 15. The closure captures factor (3) and multiplies: 5 * 3 = 15.

Which trait does a closure implement if it only reads its captured values?

  • Fn
  • FnMut
  • FnOnce
  • Copy

Answer: Fn. A read-only closure implements Fn and can be called many times.

Which trait describes a closure that mutates a captured value?

  • Fn
  • FnMut
  • FnOnce
  • Drop

Answer: FnMut. FnMut closures mutate captured state and require mutable access to be called.

Which trait describes a closure that consumes (moves out) a captured value, callable at most once?

  • Fn
  • FnMut
  • FnOnce
  • Iterator

Answer: FnOnce. FnOnce closures take ownership of a capture, so they can be called only once.

What does this print? let mut count = 0; let mut inc = || { count += 1; count }; println!("{} {} {}", inc(), inc(), inc());

  • 0 1 2
  • 1 2 3
  • 3 3 3
  • 1 1 1

Answer: 1 2 3. The FnMut closure increments and returns count each call: 1, then 2, then 3.

What does the move keyword do to a closure's captures?

  • Borrows them
  • Forces the closure to take ownership of them
  • Copies them lazily
  • Deletes them

Answer: Forces the closure to take ownership of them. move makes the closure own its captures so it can outlive the scope it was created in.

What does this print? fn make_adder(n: i32) -> impl Fn(i32) -> i32 { move |x| x + n } let add_ten = make_adder(10); println!("{}", add_ten(7));

  • 10
  • 7
  • 17
  • 70

Answer: 17. make_adder(10) returns a closure adding 10, so add_ten(7) is 17.

When is move almost always required?

  • For one-line closures
  • When sending a closure to another thread
  • When the closure takes no parameters
  • When using type inference

Answer: When sending a closure to another thread. Closures sent to another thread must own their captures, so they need move.

Continue this course

Frequently asked questions

What is the difference between a closure and a regular function?

The defining difference is that a closure can capture variables from the scope in which it is defined, while a plain fn cannot. A closure written as |x| x + factor automatically pulls factor in from the surrounding code. Closures also have lighter syntax — parameters go between vertical bars and types are usually inferred — and they are typically used inline, for example as arguments to iterator methods. A function is a named, standalone definition that only sees its parameters and globals.

What are the Fn, FnMut, and FnOnce traits?

They describe how a closure uses its captured environment. Fn closures only read captured values, so they can be called many times and shared. FnMut closures mutate captured values, so calling them requires mutable access. FnOnce closures consume (move out) a captured value, so they can be called at most once. Every closure implements one or more of these automatically based on what it does, and functions that accept closures specify which trait they need — for example, impl Fn(i32) -> i32 for a read-only closure.

When do I need the move keyword?

The move keyword forces a closure to take ownership of the variables it captures rather than borrowing them. You need it when the closure must outlive the scope where it was created — for example, when you return a closure from a function or send it to another thread. Without move, the closure would hold references that become invalid once the surrounding function returns, which the compiler rejects. With move, the captured values are owned by the closure and travel with it.

How do closures work with iterators?

Closures are the natural partner of iterator methods. Methods like map, filter, and for_each each take a closure that describes what to do with each element — for instance numbers.iter().map(|x| x * 2). The closure can capture surrounding variables, which makes these chains concise and expressive. This pairing of closures and iterators is one of the most idiomatic and powerful patterns in Rust, and it is the focus of the very next lesson.