IMPORTANT: To view this page as Markdown, append `.md` to the URL (e.g. /docs/manual/basics.md). For the complete Mojo documentation index, see llms.txt.
Skip to main content
Version: Nightly
For the complete Mojo documentation index, see llms.txt. Markdown versions of all pages are available by appending .md to any URL (e.g. /docs/manual/basics.md).

Closures

A closure is a function bundled together with values from its surrounding scope. You define it in one place and pass it somewhere else to run. The closure carries captured data with it. The compiler transforms it to a type with both behavior and storage. The code executing the closure doesn't need to know where that data came from.

This allows closures to carry state and configuration into code that executes later or elsewhere. Configuration uses capture conventions to specify how the closure interacts with captured values, choosing between read-only references, mutable references, copies, or moves.

Mojo closures look like nested functions, but they use a special syntax. Curly braces after the argument list form a capture list that declares which outer values the closure captures and how it interacts with them.

The capture list is what distinguishes a closure from an ordinary nested function. It gives the compiler the information needed to manage captured values safely and eliminate ambiguity. When you specify how a value is captured, the compiler can enforce correct usage:

def main():
var multiplier = 3

def scale(x: Int) {read multiplier} -> Int:
return x * multiplier

print(scale(5)) # 15

scale is a closure. It captures multiplier from the enclosing scope using its capture list ({read multiplier}). When you call scale(5), the closure multiplies 5 by the captured value of multiplier and returns 15.

Without a capture list, the inner function can't see anything outside its own arguments.

Why closures matter

Closures package behavior together with the data that behavior needs. You define the work in one place, then pass that package for execution later, elsewhere, or on different hardware.

This separation between defining work and executing work is central to how Mojo expresses computation.

Closures already appear throughout the Mojo standard library. Functions such as parallelize, vectorize, and elementwise accept closures. You describe the work; the library decides how to distribute it across SIMD lanes, threads, or cores.

GPU kernels extend the same idea. A closure captures the values a kernel needs, then executes across a hardware dispatch geometry. This pattern appears in reductions and accumulations, where a closure captures running state and updates it while processing elements. It also appears in custom iteration, where the iterator controls traversal and the closure controls behavior.

In many languages, closures also power asynchronous programming. You pass a closure as a callback that runs after an operation completes, carrying the state and cleanup logic it needs.

Mojo doesn't yet support async execution or escaping closures (closures that outlive their enclosing scope), but the underlying model is the same: closures define what should happen, while something else decides when and where it runs.

The capture list

A capture list such as {read x, mut y} tells the compiler how the closure captures and uses values from the surrounding scope.

For this list, x is captured as an immutable reference. The closure can read it but can't modify it. y is captured as a mutable reference, so the closure can modify it and those changes are visible in the outer scope.

Mojo requires captures to be explicit. In a systems language, knowing exactly which values a closure holds, and whether it reads, copies, or takes ownership of them, matters for both performance and correctness.

Capture by immutable reference: read

Use read when the closure needs to see a value but not change it:

def main():
var threshold = 100

def is_over(x: Int) {read threshold} -> Bool:
return x > threshold

print(is_over(50)) # False
print(is_over(200)) # True

The closure reads an immutable reference to threshold. It sees the current value each time it's called, including changes made after the closure was created:

def main():
var limit = 10

def check(x: Int) {read limit} -> Bool:
return x < limit

print(check(5)) # True
limit = 3
print(check(5)) # False (sees updated limit)

Because read captures a reference, the closure reflects the live state of the original value.

To capture every used outer value by immutable reference without naming each one, use {read}:

def main():
var a = 1
var b = 2

def sum_ab() {read} -> Int:
return a + b

print(sum_ab()) # 3

Capture by mutable reference: mut

Use mut when a closure needs to modify a captured value and make those changes visible in the enclosing scope:

def main():
var total = 0

def accumulate(x: Int) {mut total}:
total += x

accumulate(10)
accumulate(20)
print(total) # 30

Changes to total inside the closure modify the original variable directly. This is a mutable reference, not a copy.

The implicit form {mut} captures every used outer value with a mutable reference:

def main():
var count = 0
var items = List[String]()

def record(name: String) {mut}:
items.append(name)
count += 1

record("alpha")
record("beta")
print(count) # 2
print(items) # ['alpha', 'beta']

Capture by copy: var

Use var when the closure needs its own independent copy of a value. Changes to the original don't affect the closure, and changes inside the closure don't affect the original.

def main():
var snapshot_val = 42

def frozen() {var snapshot_val} -> Int:
return snapshot_val

snapshot_val = 999
print(frozen()) # 42 (captured the value at definition time)

The closure copied snapshot_val when it was created. Later changes to snapshot_val in the outer scope don't affect the closure's copy.

The implicit form {var} copies every used outer value:

def main():
var x = 10
var y = 20

def snap() {var} -> Int:
return x + y

x = 0
y = 0
print(snap()) # 30 (uses copied values)

Move capture: var name^

Use var name^ to transfer ownership of a value into the closure. The closure consumes the outer binding, which can't be used after the closure is created:

def main():
var data: List[Int] = [1, 2, 3]

def take_data() {var data^}:
print(data)

take_data() # [1, 2, 3]
# data can't be used here: ownership transferred to the closure
# print(data) # Uncomment for error: 'data' is uninitialized after move

Move capture avoids a copy entirely. The value moves into the closure's storage. This is useful for types that are expensive to copy or for transferring unique ownership.

Copyable closures: var^

The {var^} capture list moves all referenced outer values into the closure. When the captured types are Copyable, the closure value also becomes copyable.

This allows the closure itself to be assigned to new variables or passed by value.

def main():
var label = "sensor-1"

def tag() {var^} -> String:
return label

var also_tag = tag # copies the closure (and its captures)
print(tag()) # sensor-1
print(also_tag()) # sensor-1

Without {var^}, closures can't be assigned to new variables or copied.

Caller-determined mutability: ref

Use {ref name} when the closure's mutability depends on the caller's context. If the caller provides a mutable reference, the closure captures mutably. If immutable, the closure captures immutably.

The following example uses comptime if origin_of(items).mut to inspect how the closure captures the value at each call site:

def show_mutability(ref items: List[Int]):
def report() {ref items}:
comptime if origin_of(items).mut:
print("mut")
else:
print("immut")
report()

# Show immutability: `xs` uses the default `read` argument convention
def from_read(xs: List[Int]):
show_mutability(xs) # xs is an immutable reference here

# Show mutability: `xs` uses the `mut` argument convention
def from_mut(mut xs: List[Int]):
show_mutability(xs) # xs is a mutable reference here

def main():
var nums: List[Int] = [10, 20, 30]
from_read(nums) # immut
from_mut(nums) # mut

{ref name} doesn't choose a mutability. It shares the captured name's existing origin. The mutability is whatever that origin already carries, decided wherever name was bound. This is often the function's own ref parameter, ultimately resolved at the call site.

Empty capture list: {}

An empty capture list means the closure uses nothing from its surrounding scope. It's a plain function that happens to be defined inside another function:

def main():
def doubled(x: Int) {} -> Int:
return x * 2

print(doubled(5)) # 10

The body may only use its own arguments. Referencing any outer value is a compile error:

# This example doesn't compile

def main():
var a = 42

def wrong() {}:
print(a) # error: no capture convention for 'a'

Mixing capture conventions

A capture list is a comma-separated sequence of independent entries. Each entry specifies its own convention, and conventions don't carry over from one entry to the next.

def main():
var config = "prod"
var count = 0
var label = "run-1"

def process() {read config, mut count, var label}:
count += 1
print(config, count, label)

process() # prod 1 run-1
label = "run-2"
process() # prod 2 run-1 (label was copied at definition time)

Each entry is self-contained: read config is a read-only reference, mut count is a mutable reference, and var label is a copy. A bare name without a convention keyword defaults to read:

def main():
var x = 10

def show() {x}: # same as {read x}
print(x)

show() # 10

Setting a default convention

A convention list can mix implicit and explicit entries, such as {read, mut count, var label}. You may use at most one implicit entry per capture list, and you can place the entries in any order. {mut count, var label, read} is equivalent to {var label, read, mut count}.

For example:

def main():
var a = 1
var b = 2
var z = "snapshot"

def mixed() {mut, var z}:
a += 10
b += 20
print(a, b, z)

mixed() # 11 22 snapshot
z = "changed"
mixed() # 21 42 snapshot
# z was copied at def-time
# Changes to outer z don't reach the closure

Closures in practice

Configurable behavior

Closures let you build specialized behavior from general-purpose parts. The following generic function accepts any callable with the expected signature. The closure carries the configuration:

# `G` matches any `def(String) -> None` callable
def greet_all[G: def(String) -> None](names: List[String], greet: G):
for n in names:
greet(n)

def main():
var names: List[String] = ["Alice", "Bob"]
var greeting = "Hello"

def greeter(name: String) {read greeting}:
print(greeting + ", " + name + "!")

greet_all(names, greeter)
# Hello, Alice!
# Hello, Bob!

greeting = "Hi"
greet_all(names, greeter)
# Hi, Alice!
# Hi, Bob!

The inner function greeter captures greeting as an immutable read-only reference.

greet_all doesn't know anything about the greeting itself. It only knows how to call a function with the type def(String) -> None.

Because the closure captures greeting by reference instead of by copy, changes to greeting between calls are visible inside the closure.

Accumulating state

Closures with mut captures can build up results across multiple calls.

def main():
var log = List[String]()

def record(event: String) {mut log}:
log.append(event)

record("started")
record("processed item")
record("finished")

for entry in log:
print(entry)
# started
# processed item
# finished

The closure record mutates log in the outer scope. Each call appends to the same list without passing it as an argument.

Separating logic from execution

The standard library uses closures to separate what you compute from how it gets executed. You provide the logic; the library handles parallelism, vectorization, or hardware dispatch.

from std.algorithm import parallelize

def main():
var results = List[Int](length=8, fill=0)

def work(i: Int) {mut results}:
results[i] = i * i

parallelize(work, 8)
print(results) # [0, 1, 4, 9, 16, 25, 36, 49]

You define work with the logic for a single element. parallelize calls it across available threads. Each call to work accesses the same results through mutable capture. The squared values land in the outer list, ready to use after parallelize finishes.