What Is the Difference Between a Closure and a Function Pointer in Rust?

Closures capture their environment and are anonymous, while function pointers reference named functions without capturing context.

The baggage problem

You are building a configuration loader. You have a list of plugins, and each plugin needs a validation rule. Some rules are simple: "check if the value is positive." Others depend on context: "check if the value matches the region code stored in config."

You write a function to apply the rules. You try to pass a simple function for the positive check. It works. You try to pass an inline check for the region code that grabs config, and the compiler rejects you with E0308 (mismatched types). You have two ways to describe "a chunk of code to run later," and Rust treats them as fundamentally different types. One is a static address. The other is a data structure carrying state.

Function pointers: just an address

A function pointer is a reference to a named function. It is a static address in the binary. The type is fn(Args) -> Ret. This type has a fixed size equal to a pointer. It carries no state. It cannot capture variables. It points to code that exists independently of any runtime environment.

/// A named function that can be referenced by a function pointer.
fn double(n: i32) -> i32 {
    n * 2
}

fn main() {
    // Function pointer type: fn(i32) -> i32.
    // This is a direct address to the `double` symbol.
    let fp: fn(i32) -> i32 = double;

    // Calling through the pointer is a direct jump.
    println!("{}", fp(5)); // 10
}

Function pointers are cheap. You can store them in structs without heap allocation. You can pass them across FFI boundaries to C code. The compiler knows exactly where the code lives. There is no vtable lookup. There is no indirection beyond the pointer itself.

Function pointers are static. They point to code that lives in the binary and never changes.

Closures: structs in disguise

A closure is an anonymous function that captures variables from its environment. When you write |args| body, the compiler generates a unique, unnameable struct. This struct holds the captured variables as fields. It implements one of the Fn traits to make the struct callable.

The type of a closure is unique to that specific closure. You cannot name it. You cannot store it in a variable without type inference or trait objects. If two closures capture different variables, or even the same variables in a different order, they have different types.

fn main() {
    let offset = 10;

    // The compiler generates a unique struct like:
    // struct ClosureImpl { offset: i32 }
    // This struct implements Fn(i32) -> i32.
    let add_offset = |n| n + offset;

    // Calling invokes the Fn trait implementation.
    println!("{}", add_offset(5)); // 15
}

The closure captures offset by reference by default. The generated struct holds a reference to offset. If you need to mutate the captured variable, the closure captures by mutable reference. If you need to move the variable into the closure, the closure owns the value.

A closure is never just a function. It is a data structure that happens to be callable.

The trait hierarchy: Fn, FnMut, FnOnce

Closures implement traits that define how they can be called. The traits form a hierarchy: FnOnce is the base, FnMut extends it, and Fn extends that. The trait you get depends on what the closure does with its captures.

FnOnce means the closure can be called once. It consumes self. This happens when the closure moves a captured value out of the struct. For example, if the closure captures a Vec and calls .pop(), it must take ownership of the Vec to mutate it.

FnMut means the closure can be called multiple times and may mutate its captures. It takes &mut self. This happens when the closure modifies a captured variable.

Fn means the closure can be called multiple times and does not mutate its captures. It takes &self. This happens when the closure only reads captured variables.

fn main() {
    let mut counter = 0;

    // Captures `counter` by mutable reference.
    // Implements FnMut because it mutates the capture.
    let increment = || {
        counter += 1;
    };

    // Calling requires mutable borrow of the closure.
    increment();
    increment();
    println!("{}", counter); // 2
}

The compiler picks the most specific trait the closure satisfies. If a closure only reads captures, it implements Fn, which automatically implements FnMut and FnOnce. This allows you to pass an Fn closure to a function expecting FnMut.

Match the trait to the capture. If you mutate, you need FnMut. If you move, you need FnOnce.

Coercion: when a closure becomes a pointer

Here is the surprising part. If a closure captures nothing, the compiler can coerce it to a function pointer. The closure type is still a unique struct, but the compiler allows it to convert to fn when the context demands it.

This coercion happens automatically when you assign a non-capturing closure to a variable with type fn, or when you pass it to a function expecting fn.

fn main() {
    // This closure captures nothing.
    // It coerces to fn(i32) -> i32 because of the type annotation.
    let fp: fn(i32) -> i32 = |n| n * 2;

    // This closure captures `x`.
    // Coercion fails. E0308: mismatched types.
    let x = 10;
    // let fp_bad: fn(i32) -> i32 = |n| n + x; // Error
}

The coercion is a convenience. It lets you use closure syntax for simple static functions without writing fn. It does not change the fact that closures are structs. The moment you capture a variable, the coercion breaks.

Coercion is a convenience, not a guarantee. If you capture, the coercion breaks.

Storing callbacks: the cost of flexibility

Function pointers and closures behave very differently when you store them in structs. A function pointer has a known size and type. You can store it directly in a struct field. A closure has a unique type. You cannot store it directly unless you use generics or trait objects.

/// Struct with a function pointer field.
/// Zero overhead. Direct call.
struct StaticValidator {
    check: fn(i32) -> bool,
}

/// Struct with a closure field.
/// Requires heap allocation and dynamic dispatch.
struct DynamicValidator {
    // Box<dyn Fn(i32) -> bool> erases the unique closure type.
    check: Box<dyn Fn(i32) -> bool>,
}

fn main() {
    // Storing a function pointer is trivial.
    let static_v = StaticValidator {
        check: |n| n > 0, // Coerces to fn
    };

    // Storing a closure requires boxing.
    let threshold = 10;
    let dynamic_v = DynamicValidator {
        check: Box::new(|n| n > threshold),
    };
}

Box<dyn Fn> uses a trait object. The box holds a pointer to the heap-allocated closure struct and a pointer to a vtable. Calling through the box involves an indirect call via the vtable. This adds allocation cost and dispatch overhead.

Generics offer a middle ground. If you define a function or struct with a generic parameter bounded by Fn, the compiler monomorphizes the code. You get zero-cost polymorphism, but the type size grows with each unique closure type.

Pay for what you use. If you don't need captures, don't pay for the heap allocation.

Pitfalls and compiler errors

The most common error is E0308 (mismatched types) when you try to assign a capturing closure to a function pointer type. The compiler sees the closure struct and the fn type and refuses the assignment.

fn main() {
    let x = 5;
    // E0308: mismatched types.
    // Expected fn(i32) -> i32, found closure.
    let fp: fn(i32) -> i32 = |n| n + x;
}

The fix is to either remove the capture or change the type to accept a closure. If you control the API, use a generic bound or a trait object. If you are calling an API that requires fn, you must extract the logic into a named function or ensure the closure captures nothing.

Another error is E0277 (trait bound not satisfied). This happens when you pass a closure to a function expecting a specific Fn trait, but the closure doesn't implement it. For example, passing a FnOnce closure to a function expecting Fn.

fn call_twice<F: Fn()>(f: F) {
    f();
    f();
}

fn main() {
    let data = vec![1, 2, 3];
    // E0277: closure cannot be called more than once.
    // The closure moves `data`, so it implements FnOnce, not Fn.
    call_twice(|| {
        let _ = data;
    });
}

The closure moves data into the struct. It can only be called once. The function call_twice requires Fn, which implies multiple calls. The compiler rejects the mismatch.

Read the type error. If it says "closure", you are trying to stuff a backpack into a mailbox.

Decision: function pointer vs closure

Use function pointers when you need to store a callback in a struct field without heap allocation or vtable overhead. Use function pointers when the logic is static, pure, and does not depend on local variables. Use function pointers for FFI boundaries where C expects a void (*)(void).

Use closures when you need to capture local state and pass logic inline. Use closures with generic parameters when you want zero-cost polymorphism and the compiler can monomorphize the code. Use closures for iterators and combinators like map and filter where the capture makes the code readable.

Use Box<dyn Fn> when you must store heterogeneous callbacks in a collection or struct and cannot use generics. Accept the heap allocation and dynamic dispatch cost for the flexibility.

Choose the tool that matches the state. No state means function pointer. State means closure.

Where to go next