---
title: Closures
source: 'https://academia.sh/en/courses/rust-abstraction/closures'
course: 'Traits, Generics and Error Handling'
language: en
updated: '2026-09-15T04:58:42+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Compares, by running it, how many times the same job runs when written as a direct call, a deferred closure, and a closure that is never called'
    - 'Shows that the lifetime of a value captured by move is bound to the closure itself, independent of whether the body ever ran'
    - "Distinguishes a closure's own definition from a separate act that never enters the run ledger"
---

# Closures

A closure's body does not run on the line where it is defined but at the moment it is called: the same job is written as a direct call, a deferred call, and a closure that is never called, and the run counts are compared; what moving versus capturing by reference does to the source value is also measured.

The previous topic measured that the rest of a body might never run at all: an early
return or an unrecoverable error silently skipped the steps written in the source. That
measurement looked at which branch ran — the body itself was always in the same call. This
lesson takes the same "how many times did it run" question down to a smaller unit: not the
branch, but the way the body is stored. Whether a body runs even once now depends on how it
was stored before being called. A closure is a value that can capture something from its
surroundings and defer a body to later; that definition was established in the Programming
Fundamentals course and is not rewritten here. What this lesson measures is how many times
the same job runs across three storage forms, how many times calling the same closure
repeatedly runs its body, and what capturing does to the source value. All three
measurements use the same run ledger: when a body runs, it writes its own name into this
ledger; when it does not run, it leaves no trace. The count is a record read from the run
itself, not a guess.

## The Same Job, Three Ways of Being Called

A body can be written in three forms: as an ordinary function that is defined and called
immediately; wrapped in a closure, stored under a name, and called later; or wrapped in a
closure and never called at all. The source text is nearly identical in all three — the
difference lies in the distance between where it is defined and where it is called. For an
ordinary function call this distance is zero. Once a closure is stored, this distance opens
up, and the body becomes callable whenever its name is held — or never called at all. The
distance itself is not what is measured; what is measured is whether the body ends up being
called at all.

In the run below, `direct_body` is not a closure but an ordinary function defined inside
another function; this is deliberate, because a nested function in Rust cannot capture
anything from its surroundings — `ledger` is passed to it explicitly. Closure syntax (`||`),
by contrast, turns a body into a value bound to the names around it; this difference is
noted so the comparison stays fair. What is being compared is not "is it a closure" but the
distance between definition and call.

**CI1.** — the claim that "the same job runs on a direct call, does not run until called on
a deferred closure, and never runs by the end of the run on a closure that is never called"
is confirmed by the run below.

```rust
// closure_body.rs — the same job in three forms: direct call, deferred call, closure never called
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;

type Ledger = Rc<RefCell<BTreeMap<String, u32>>>;

fn record(ledger: &Ledger, name: &str) {
    *ledger.borrow_mut().entry(name.to_string()).or_insert(0) += 1;
}

fn times(ledger: &Ledger, name: &str) -> u32 {
    *ledger.borrow().get(name).unwrap_or(&0)
}

fn main() {
    let ledger: Ledger = Rc::new(RefCell::new(BTreeMap::new()));

    // direct body: not a closure, an ordinary function defined and called right away
    fn direct_body(ledger: &Ledger) {
        record(ledger, "direct-body");
    }
    direct_body(&ledger);

    // deferred body: stored in a closure, does not yet run on the line where it is defined
    let deferred = || record(&ledger, "deferred-body");
    println!("right after being defined: {}", times(&ledger, "deferred-body"));
    deferred();
    println!("after calling: {}", times(&ledger, "deferred-body"));

    // never-called closure: the same slice as the course's measurement core, never called
    let _closure = || record(&ledger, "uncalled-closure");

    println!("direct body: {}", times(&ledger, "direct-body"));
    println!("uncalled closure: {}", times(&ledger, "uncalled-closure"));
}
```

```
right after being defined: 0
after calling: 1
direct body: 1
uncalled closure: 0
```

The deferred body has not yet run on the line where it is defined: the counter read right
after the closure is defined is zero. Once it is explicitly called, the counter jumps to
one — no matter how many lines, calls, or branches sit between definition and call, the
counter would stay at zero until `deferred` is called. The third body, `uncalled-closure`,
is the exact slice from the course's measurement core: it is stored under the name
`_closure` and is never called anywhere else in the program. This line falls into the
**never** class in the course's binding table — being written in the source does not mean
having run. This measurement is not to be confused with the question the JVM, Concurrency
and Performance course asks — which thread runs a given step: there is only one thread here,
and the only question is how many times the body ran.

## A Closure's Body Runs As Many Times As It Is Called

The previous measurement looked at whether the same body ran even once. But a closure is
not limited to a single call: the same closure stored under one name can be called
repeatedly, and every call re-runs its body — the previous result is never remembered,
stored, or reused. What sets a closure apart from an ordinary value is that its body is
bound to a name and can be called as many times as wanted. This is a different question
from the gap between "never" and "at least once": a closure may never be called, but the
same closure may also be called many times, and in that second case the counter tracks the
call count exactly.

**CI2.** — the claim that "calling the same closure three times in a row runs its body
three times, not once" is confirmed by the run below, where three separate calls raise the
counter to three.

```rust
// multi_call.rs — a closure whose body only reads a captured value can be called repeatedly
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;

type Ledger = Rc<RefCell<BTreeMap<String, u32>>>;

fn record(ledger: &Ledger, name: &str) {
    *ledger.borrow_mut().entry(name.to_string()).or_insert(0) += 1;
}

fn times(ledger: &Ledger, name: &str) -> u32 {
    *ledger.borrow().get(name).unwrap_or(&0)
}

fn main() {
    let ledger: Ledger = Rc::new(RefCell::new(BTreeMap::new()));
    let label = String::from("example-label");

    let multi_called = || {
        record(&ledger, "multi-called-body");
        label.len()
    };

    let a = multi_called();
    let b = multi_called();
    let c = multi_called();

    println!("lengths after three calls: {a}, {b}, {c}");
    println!("how many times the body ran: {}", times(&ledger, "multi-called-body"));
}
```

```
lengths after three calls: 13, 13, 13
how many times the body ran: 3
```

`multi_called` only reads the `label` it captured — it returns its length, never carrying
the value itself out of its body. This is why the same closure can be called three times,
and each call raises the counter by one: three calls, three runs. Had this body instead
carried the value's ownership out — for instance, returning `label` itself — the first call
would have let the value slip away, and the compiler would have rejected the second call: a
body can no longer hand out a value it no longer holds. This rejection is not put into the
source, because a source that does not compile falls outside the lesson's scope; but the
result stands — a closure that only reads its captured value can be called as often as
wanted, while one that consumes it can only be called once. Both are the same ownership
rule, reflected differently in the call count. These three degrees of callability — as
often as wanted, only by mutating, only once — correspond to the three closure traits in
Rust's standard library; their names and the boundary between them fall outside this
lesson's scope, since reading a contract's boundary is a separate lesson's subject. What is
measured here is only the call count's reflection back onto the source.

## What Capturing Does to the Source Value

Whether closures run is one half of the question; what they capture is the other. A closure
can take a value from its surroundings in two ways: by borrowing (by reference) or by
moving. This choice is not explicit in the body's source text — the compiler decides for
itself by looking at what the body does with each captured name; the `move` keyword forces
this decision by hand. Moving is the closures' version of the ownership rule built in the
Ownership and the Memory Model course: once a value is moved into a closure, that closure
itself becomes the value's owner, and the value's lifetime becomes bound to the closure's
lifetime — even if its body never runs.

**CI3.** — the claim that "a value captured by moving is dropped together with the closure
even if the closure's body never runs" is confirmed by the run below, where the
`captured-value-dropped` counter stays at zero until the closure is dropped and then jumps
to one, while the body's own counter stays at zero throughout the run.

```rust
// capture_lifetime.rs — a value captured by moving has its lifetime bound to the closure itself
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;

type Ledger = Rc<RefCell<BTreeMap<String, u32>>>;

fn record(ledger: &Ledger, name: &str) {
    *ledger.borrow_mut().entry(name.to_string()).or_insert(0) += 1;
}

fn times(ledger: &Ledger, name: &str) -> u32 {
    *ledger.borrow().get(name).unwrap_or(&0)
}

struct Tracked {
    ledger: Ledger,
}

impl Drop for Tracked {
    fn drop(&mut self) {
        record(&self.ledger, "captured-value-dropped");
    }
}

fn main() {
    let ledger: Ledger = Rc::new(RefCell::new(BTreeMap::new()));
    let tracked = Tracked { ledger: Rc::clone(&ledger) };

    // the closure captures "tracked" by moving it: the name "tracked" cannot be used after this line
    let never_called = move || {
        record(&tracked.ledger, "uncalled-closure-2");
    };
    println!(
        "after the closure is defined, before ever being called: {}",
        times(&ledger, "captured-value-dropped")
    );

    drop(never_called);
    println!("after the closure is dropped: {}", times(&ledger, "captured-value-dropped"));
    println!("its body never ran: {}", times(&ledger, "uncalled-closure-2"));
}
```

```
after the closure is defined, before ever being called: 0
after the closure is dropped: 1
its body never ran: 0
```

The `Tracked` value is moved into the closure with the `move` keyword; after this line the
name `tracked` can no longer be used — the closure has become the value's owner. When the
closure is dropped without ever being called, the moved `Tracked` value ends along with it,
and the drop leaves its mark in the ledger. This is an event entirely separate from the
body's run counter: `uncalled-closure-2` stays at zero for the whole run, while the counter
for the captured value's drop jumps to one. The closure's own definition, and its holding
onto the moved value, leaves a real effect on the source even though its body is never
called — only this effect is not what the run counter measures. Even without the explicit
`drop` call, the same drop would have happened once the closure's scope ended; the explicit
call here only made the moment observable, and did not change the outcome.

Capturing by reference, unlike moving, keeps the source name alive; the borrowing rules
established in the Ownership and the Memory Model course apply here too, and this lesson
does not rebuild them.

**CI4.** — the claim that "a closure that captures by reference leaves the name it captured
usable both after the closure is defined and after the closure is called" is confirmed by
the run below, where the name `label` is readable in all three lines.

```rust
// capture_by_reference.rs — capturing by reference keeps the source name alive, unlike moving
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;

type Ledger = Rc<RefCell<BTreeMap<String, u32>>>;

fn record(ledger: &Ledger, name: &str) {
    *ledger.borrow_mut().entry(name.to_string()).or_insert(0) += 1;
}

fn times(ledger: &Ledger, name: &str) -> u32 {
    *ledger.borrow().get(name).unwrap_or(&0)
}

fn main() {
    let ledger: Ledger = Rc::new(RefCell::new(BTreeMap::new()));
    let label = String::from("measurement-label");

    let captures_by_reference = || {
        record(&ledger, "reference-captured-body");
        label.len()
    };

    println!("after the closure is defined, label is still readable: {label}");
    let length = captures_by_reference();
    println!("after the call: {}", times(&ledger, "reference-captured-body"));
    println!("label is still reachable after the closure: {label} (length {length})");
}
```

```
after the closure is defined, label is still readable: measurement-label
after the call: 1
label is still reachable after the closure: measurement-label (length 17)
```

`captures_by_reference` is defined without the `move` keyword, and its body only reads
`label`; this is why capturing reduces to a borrow rather than a move. The name `label` can
be read directly both right after the closure is defined and after it is called — a full
contrast with the previous run, where `tracked` became unusable right after being moved.
The same capture syntax (`|| { ... }`) produces two different ownership outcomes depending
on what the body does with the value it captures: one closes off the source name, the other
leaves it open. This lesson does not rebuild these two outcomes, it only measures that both
are independent of the body's run count. Which path gets chosen is read from whether `move`
is written in the source; how many times the body runs is a separate axis, set only by how
many times the closure is called.

## A Bounding Measurement

The run ledger only counts body calls; a closure being **defined** is also work, but this
work never enters the ledger. When a closure is defined, the values it captures (if any)
are copied or moved right then, and the closure itself, as a value, takes up a place in
memory — real work, but none of it involves a counter call, because what triggers a call is
not the body we wrote but the closure representation the compiler produces. This is why
`uncalled-closure`'s counter being zero does not mean the closure costs nothing; it only
means its body never ran. What it costs to build the closure falls outside the scope of the
number this lesson measures.

This boundary looks small for a single closure but grows once several are built one after
another. If a closure is itself a value, that value can be placed in another structure,
passed as a parameter, or become a link in a chain — and every link takes up a place at the
moment it is built, even if it never runs. This will matter directly in the next topic: once
several closures are chained into a single traversal contract, the contract itself gets
built, but its bodies do not run unbidden. This lesson measured the first link of that
chain — a single closure.

Put together, these four runs sketch the following picture: whether a body runs depends on
how it is stored, how many times it runs depends on the call count, and which value ends
when depends on how it was captured — and none of these three questions determines the
others. A closure can hold a value and drop it without ever being called; it can be called
many times without ever moving any value; or it can consume its captured value and be
limited to a single call. The source can suggest which answer each question will give, but
the exact number is only ever read from the run.

## Summary

- The same job can be written in three forms: a direct call, a deferred closure, a closure
  that is never called; among closures, only the ones that are called actually run.
- The `uncalled-closure` body from the course's measurement core did **not** run in this run
  at all — being written in the source does not mean having run.
- If the same closure is called several times, its body re-runs on every call (three calls,
  three runs); a closure that only reads its captured value can be called as often as
  wanted, while one that carries the value out of its body can only be called once.
- The lifetime of a value captured by moving is bound to the closure itself; even if the
  body never runs, the captured value is dropped together with the closure.
- Capturing by reference, unlike moving, keeps the source name alive — unlike moving, it
  does not change the value's ownership.
- A closure's own definition is a separate act that does not enter the run ledger; the
  count only ever measures body calls.

## Next Step

In this lesson, a closure's body was stored on its own and ran or did not run depending on
whether it was called; how many times it was called was reflected directly in the run
count. The next lesson carries the same question from a single closure to a chain: when
many closures join around the same contract, the contract itself — the iterator — decides
when this chain advances. That a built chain never enters the run ledger without a single
call to trigger it will be measured as a direct extension of this lesson's
`uncalled-closure` example.
