How to pass closure to function

Pass a closure to a function by using a generic parameter with a trait bound like Fn, FnMut, or FnOnce.

The problem with rigid functions

You have a vector of sensor readings. You need to process them. Sometimes you want to square the values. Sometimes you want to clamp them to a safe range. Sometimes you need to apply a complex calibration formula that depends on a temperature offset sitting in a local variable. Writing a separate function for every variation is tedious. You want a single process_readings function that accepts the transformation logic as an argument. That logic is a closure.

Rust makes this easy, but the syntax can look dense at first. You'll see generics, trait bounds like Fn, and angle brackets everywhere. The underlying idea is simple. You're passing a chunk of code as a value, just like you pass an integer or a string. The compiler needs to know what that chunk of code expects as input, what it returns, and how it interacts with variables from the surrounding scope.

Closures are functions with baggage

A named function is a fixed recipe. It lives in the module, takes arguments, and returns a result. A closure is a custom instruction you write inline. It can capture variables from the scope where it's defined. This capture is the "baggage."

Think of a named function as a standard tool in your toolbox. A closure is a custom jig you build on the spot. It grabs whatever it needs from the workbench and does the job. If the jig needs a specific wrench, it holds onto that wrench. If it needs to modify a part on the bench, it reaches out and changes it.

Rust tracks this baggage strictly. The compiler analyzes the closure to see what it captures and how it uses those captures. Based on that analysis, the closure implements one of three traits: Fn, FnMut, or FnOnce. These traits describe the closure's behavior regarding its captured data.

  • Fn means the closure reads captured data but never modifies it. You can call this closure as many times as you want.
  • FnMut means the closure modifies captured data. You can call it multiple times, but each call might change the state it captured.
  • FnOnce means the closure consumes captured data. It takes ownership of a value and drops it, or moves it out. You can only call this closure once.

Any closure that implements Fn also implements FnMut and FnOnce. A closure that implements FnMut also implements FnOnce. This hierarchy matters when you write function parameters.

Minimal example

Here is the simplest way to pass a closure. The function is generic over a type F. The where clause constrains F to implement Fn(i32) -> i32. This tells the compiler that F can be called with an i32 and returns an i32.

/// Applies a transformation to a number and returns the result.
fn apply<F>(x: i32, operation: F) -> i32
where
    // The closure must accept an i32 and return an i32.
    // It must implement Fn, meaning it doesn't mutate captured state.
    F: Fn(i32) -> i32,
{
    // Call the closure with the input value.
    // The compiler inlines this call for performance.
    operation(x)
}

fn main() {
    // Define a closure that doubles its input.
    // The compiler infers the type of `x` as i32 from the call site.
    let double = |x| x * 2;

    // Pass the closure to the function.
    // Rust passes the closure by value.
    let result = apply(5, double);
    println!("Result: {result}");
}

The closure |x| x * 2 has a unique type that you cannot name. It's not fn(i32) -> i32. It's a compiler-generated struct that implements Fn(i32) -> i32. The generic parameter F allows apply to accept this unique type. The compiler generates a specialized version of apply for the exact type of double. This process is called monomorphization. It gives you the flexibility of dynamic dispatch with the performance of a static function call.

Why generics instead of a concrete type?

You cannot write a function signature like fn apply(x: i32, operation: ClosureType) -> i32 because ClosureType doesn't exist in Rust syntax. Every closure has its own distinct type, even if two closures have the same signature. The compiler generates a new type for each closure expression.

Generics solve this. By using F: Fn(...), you say "I don't care what the specific type is, as long as it implements this trait." The compiler fills in the blank for you.

There is a shorthand for this pattern. You can write impl Fn(i32) -> i32 directly in the argument list. This is syntactic sugar for a generic bound. It reads cleaner when you don't need to name the type for other reasons.

/// Shorthand version using `impl Trait` in argument position.
fn apply_short(x: i32, operation: impl Fn(i32) -> i32) -> i32 {
    operation(x)
}

The community prefers impl Fn(...) in function arguments over F: Fn(...) when the generic isn't needed elsewhere. It reduces boilerplate. If you need to name the type for a where clause or a return type, stick to the generic.

Realistic example: Benchmarking

A common use case is wrapping a closure in a utility function. Here is a benchmark helper that runs a closure and measures its execution time. The closure might do anything: compute a hash, query a database, or render a frame. The benchmark function doesn't care. It just runs the closure and reports the time.

This example uses FnOnce. The closure might consume resources. It might move a value out of a captured variable. FnOnce is the most permissive bound. It accepts any closure, whether it's Fn, FnMut, or FnOnce.

use std::time::Instant;

/// Runs a closure and prints how long it took.
/// Returns the result produced by the closure.
fn benchmark<F, R>(name: &str, f: F) -> R
where
    // FnOnce allows the closure to consume captured data.
    // This is the most flexible bound for a "run once" operation.
    F: FnOnce() -> R,
{
    let start = Instant::now();
    
    // Execute the closure.
    // The closure is moved into this call and dropped afterward.
    let result = f();
    
    let elapsed = start.elapsed();
    println!("{name} took {elapsed:?}");
    result
}

fn main() {
    // Closure that does some work.
    // It captures nothing, so it implements Fn, FnMut, and FnOnce.
    let compute = || {
        let mut sum = 0;
        for i in 0..1_000_000 {
            sum += i;
        }
        sum
    };

    // Pass the closure. It runs once.
    let total = benchmark("Summation", compute);
    println!("Total: {total}");
}

The benchmark function returns R, the result type of the closure. This makes the wrapper transparent. You can chain the result directly. The generic R allows the closure to return anything.

Pitfalls and compiler errors

Closures interact with Rust's ownership and borrowing rules. If you get the capture mode wrong, the compiler will stop you.

Borrowing conflicts

If a closure captures a mutable reference, that borrow lasts for the lifetime of the closure. You cannot use the borrowed data while the closure exists.

fn main() {
    let mut data = vec![1, 2, 3];
    
    // Closure captures `data` by mutable reference.
    // The borrow is held for the lifetime of `update`.
    let update = || {
        data.push(4);
    };
    
    // Error: E0502.
    // Cannot borrow `data` as immutable because it is also borrowed as mutable.
    println!("{data:?}");
    
    update();
}

The compiler rejects this with E0502 (cannot borrow as mutable because it is also borrowed as immutable). The closure update holds a &mut data. The println tries to borrow data immutably. Rust prevents simultaneous mutable and immutable borrows. Drop the closure or move the print statement after the closure goes out of scope.

Consuming closures

If a closure captures a value by ownership, you can only call it once. Calling it a second time tries to move the value again.

fn main() {
    let s = String::from("hello");
    
    // Closure captures `s` by value.
    // It moves `s` into the closure.
    let greet = || println!("{s}");
    
    greet();
    
    // Error: E0382.
    // Use of moved value `greet`.
    // The closure consumed `s` on the first call.
    greet();
}

The compiler rejects this with E0382 (use of moved value). The closure implements FnOnce. It moves s when created. The first call executes the body. The second call tries to use the closure again, but the closure has already consumed its captured data. If you need to call the closure multiple times, capture by reference instead.

fn main() {
    let s = String::from("hello");
    
    // Explicitly capture by reference.
    // The closure borrows `s` instead of moving it.
    let greet = || println!("{s}");
    
    greet();
    greet(); // OK. The borrow is released between calls?
             // Actually, the closure holds the borrow.
             // But Fn allows multiple calls if the borrow is compatible.
             // Here, the closure captures `&s`.
             // It implements Fn.
             // You can call it multiple times.
}

Wait, the second example needs care. If the closure captures &s, it implements Fn. You can call it multiple times. The borrow is held by the closure, but Fn allows re-entrancy as long as the borrow is immutable. If you capture &mut s, the closure implements FnMut. You can call it multiple times, but you cannot use s while the closure exists.

Trait bound mismatches

If you write a function expecting Fn, but pass a closure that mutates captured state, the compiler complains.

fn apply_readonly<F>(f: F)
where
    F: Fn(),
{
    f();
}

fn main() {
    let mut counter = 0;
    
    // Closure mutates `counter`.
    // It implements FnMut, not Fn.
    let increment = || counter += 1;
    
    // Error: E0277.
    // The trait bound `increment: Fn` is not satisfied.
    apply_readonly(increment);
}

The compiler rejects this with E0277 (trait bound not satisfied). The closure increment implements FnMut, which is not a subtype of Fn. Fn requires the closure to not mutate captures. Change the bound to FnMut or FnOnce to accept this closure.

Decision time

Choose the right trait bound and syntax based on your needs.

Use Fn when the closure reads captured data but never modifies it. This is the most restrictive bound on the caller. It forces the caller to write a closure that doesn't mutate state. Use this when you want to guarantee the closure is side-effect-free regarding captured variables.

Use FnMut when the closure modifies captured variables. The closure needs to mutate state it captured, like a counter or a buffer. This bound accepts FnMut and FnOnce closures. It rejects Fn closures? No. Fn closures also implement FnMut. So FnMut accepts Fn closures too. FnMut is less restrictive than Fn. It allows mutation.

Use FnOnce when the closure consumes captured data. The closure takes ownership of a value and drops it, or moves it out. You can only call this closure once. This is the most permissive bound. It accepts any closure. Use this when you're writing a function that runs the closure exactly once, like a thread spawn or a retry mechanism.

Use impl Fn(...) in argument position when you want to accept any closure matching the signature without exposing a generic parameter. This is syntactic sugar for a generic bound. It reads cleaner. Use this for simple wrappers and utilities.

Use a generic F: Fn(...) when you need to name the type for other bounds or return types. If you have multiple closure parameters with relationships, or you need to return the closure type, you must use a generic.

Use Box<dyn Fn(...)> when you need to store closures of different types in a collection, or when the closure type is unknown at compile time. This trades performance for flexibility. The call goes through a vtable. Use this for plugin systems or dynamic configuration. Avoid it for performance-critical paths.

Use the move keyword when you need to force the closure to capture variables by value. This is essential when passing a closure to a thread, because the thread might outlive the current scope. The closure must own its data.

Pick the tightest bound that fits your logic. FnOnce is safer than FnMut, which is safer than Fn. If your function only calls the closure once, use FnOnce. It gives callers the most freedom. If you need to call it multiple times, use FnMut unless you can prove mutation isn't needed, in which case Fn documents that guarantee.

Trust the trait bound. It tells the compiler exactly what your closure is allowed to do. If the compiler rejects a closure, check the captures. You're probably mutating something you shouldn't, or moving a value you need later.

Where to go next