Mehdi Akiki
Rust Failure Atlas / Language and diagnostics

RFA-071 · Case file with fixtures · Case 43 of 694 · Compiler evidence

E0594: A Rust Closure Mutates Its Capture but the API Requires `Fn`

Mutating captured state needs exclusive access to the closure environment. Match the callback bound to the receiver access the API can provide, or move mutation behind an explicit synchronization or interior-mutability boundary.

Reviewed
Rust
Rust 1.98.1
Targets
all targets
Profiles
dev, release, test

Direct answer

What this Rust failure means

Why it happens
Mutation needs exclusive access to the captured environment, while an `Fn` bound promises calls through a shared reference to the closure.
First discriminating check
Change only the callback bound and local parameter to `FnMut`, then verify whether the API can honestly provide exclusive access for every call.

A callback can be callable twice and still need mutable access to itself. This API loses that distinction:

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

fn main() {
    let mut count = 0;
    call_twice(|| count += 1);
}

Rust 1.98.1 reports E0594: count cannot be assigned because it is captured in an Fn closure. The failing fixture records the full compiler explanation and its suggestion to use FnMut.

The callback count is not the problem. FnMut can be called repeatedly. The problem is which kind of reference the receiving API promises to use for each call.

Call traits describe access to the closure environment

A closure is a value containing captured state. Its call traits can be understood through their receiver:

TraitConceptual receiverWhat a call may do
Fn&selfread captures without requiring exclusive environment access
FnMut&mut selfmutate captures and call repeatedly
FnOnceselfconsume captures and call at least once

The closure || count += 1 captures count mutably. Calling it requires access to that mutable borrow, so the closure implements FnMut and FnOnce, but not Fn.

The Rust book explanation of closure call traits derives the trait from what the body does with captured values. The caller does not choose the weakest trait by annotation; the body determines what is available.

Match the API to the actual call behavior

The direct repair is:

fn call_twice<F: FnMut()>(mut callback: F) {
    callback();
    callback();
}

The parameter binding is mut because each call uses the callback through mutable access. The repaired fixture calls it twice and asserts that the captured counter reaches two under Rust 1.98.1.

An FnMut bound still accepts closures which also implement Fn. It expresses the maximum access the helper may need, not a requirement that every callback actually mutate something.

Passing the callback by value does not change this receiver rule. Ownership of the closure lets the helper obtain mutable access, but the generic bound must still permit using that access when the call operator is invoked.

Why an API may deliberately require Fn

Changing the bound is not always possible. A parallel library may call one closure concurrently through shared references. An event subscription may retain a callback behind &self. A retry component may require reentrant invocation. Those designs cannot simply provide one exclusive &mut receiver for each call.

If mutation is still required, the state needs an appropriate interior boundary. Single-threaded code can use Cell for Copy state or RefCell for dynamically checked borrowing:

let count = std::cell::Cell::new(0);
call_twice(|| count.set(count.get() + 1));

The closure itself only shares &Cell<_>, so it can implement Fn; Cell owns the mutation protocol. For multi-threaded callbacks, an atomic or lock may be required depending on the operation. Choosing one is a concurrency design, not just a trait fix.

move does not turn FnMut into Fn

Adding move transfers count into the closure environment. The body still assigns to the captured field, so calling still needs &mut self. Capture mode and call trait are related but separate.

For a Copy integer, move also means the outer count is no longer the state being observed; the closure changes its private copy. That can compile after other changes while silently breaking the intended result.

The strongest bound reduces usable callbacks

An API requiring Fn is accepting a narrower set of closure behaviors than one requiring FnMut. An API requiring FnOnce can accept all closures, but it promises only that it consumes the callback and cannot call it twice. I pick the least restrictive bound compatible with the implementation:

  • Calls only once: FnOnce is often enough.
  • Calls repeatedly with exclusive access: FnMut.
  • Calls through shared access, potentially concurrently: Fn plus any necessary Send and Sync bounds.

The official E0594 page covers assignment through an immutable context generally. For closures, my first check is to replace the syntax with the receiver table. The body mutates its environment, so either the API provides &mut access or the mutable state moves behind a type designed to support mutation through shared access.