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).

Mojo closure declarations reference

A closure is a nested function with a capture list that controls how it accesses values from its enclosing scope:

def main():
var multiplier = 3

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

print(scale(5)) # 15

{read multiplier} references multiplier from the enclosing scope as an immutable reference. Without the capture list, referencing any outer value is a compile error.

A closure is created when its enclosing def runs and exists only for the lifetime of that enclosing scope. Mojo doesn't support escaping closures or async execution.

Closure syntax

def name(argument-list) {capture-list} -> ReturnType:
body

def name[parameter-list](argument-list) {capture-list}
-> ReturnType:
body

def name(argument-list) raises {capture-list} -> ReturnType:
body

Effects (raises) go between the argument list and the capture list. The capture list appears immediately before the return arrow. It can be empty ({}) or omitted entirely; both forms prohibit references to outer values.

The argument list, parameter list, effects, return type, and where clauses follow the same rules as top-level functions. See Function declarations.

Capture list grammar

A capture list is a brace-enclosed, comma-separated sequence of entries:

FormMeaning
<conv> nameCapture name with convention <conv>
<conv>Default convention for all free variables
nameCapture name with convention read
<conv> name^Move-capture (only with var or no <conv>)

<conv> is one of read, mut, var, ref. Position within the list isn't significant: {mut, var z} and {var z, mut} are equivalent. Trailing commas are accepted.

At most one entry can omit a name (the default-convention entry). A second produces:

error: default capture convention was already specified;
remove the duplicate

The ^ marker is only legal on var entries or entries with no convention keyword:

error: '^' requires 'var' convention; write 'var x^' to
move a capture

Capture conventions

ConventionFormStorage in closureLifetime tie to outer
read{read name} / {read}Immutable referenceLive
mut{mut name} / {mut}Mutable referenceLive
ref{ref name} / {ref}Reference, mutability from originLive
var{var name} / {var}Owned copyIndependent
Move{var name^}Owned, consumed from outerConsumes outer
Copyable{var^}Owned, closure is CopyableIndependent

read

Immutable reference. The closure observes the outer value's current state at each call:

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

{read} (no name) applies read to every free variable in the body. A bare name without a convention keyword also defaults to read: {x} is equivalent to {read x}.

mut

Mutable reference. Writes inside the closure modify the outer binding:

def main():
var total = 0

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

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

{mut} (no name) applies mutable-reference capture to every free variable in the body.

var

Owned copy. The closure receives its own value, constructed by calling the type's copy constructor when the closure is declared. Later changes to the outer binding don't affect the closure's copy, and vice versa:

def main():
var snapshot = 42

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

snapshot = 999
print(frozen()) # 42

{var} (no name) copies every free variable in the body.

The copy constructor runs once per closure declaration. Capturing a large List or String by var allocates at that point. Use read or mut when an independent copy isn't needed.

Move capture: var name^

Transfers ownership of name into the closure. The outer binding is consumed; using it after the closure declaration is a compile error:

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

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

take_data() # [1, 2, 3]
# print(data) # error: 'data' is uninitialized
# after move

Move capture skips the copy that var name would perform and is the only way to capture a move-only type by value.

Constraints:

  • Only legal after var or after a bare (convention-less) name.
  • {read name^}, {mut name^}, and {ref name^} are rejected.
  • A bare name^ is equivalent to var name^. Both produce the same kConventionMove entry. The compiler accepts both forms, but mojo format currently rejects the bare form. Use {var name^} in code that must round-trip through the formatter.

Copyable closures: var^

{var^} (no name) applies move capture as the default for every free variable in the body. When every captured type is Copyable, the resulting closure value is also Copyable:

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

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

var clone = tag # closure value copied
print(tag()) # sensor-1
print(clone()) # sensor-1

Copying the closure invokes the copy constructor of each captured value. The constructor fires at the assignment, not at the closure declaration.

Constraints:

  • {var^} is a default-convention entry. At most one default-convention entry per capture list.
  • If any captured type is move-only, the closure is Movable but not Copyable.

Comparison with {var name^}:

FormCaptured namesClosure value
{var name^}Only name, by moveNot Copyable by default
{var^}All referenced names, by moveCopyable if captures are Copyable

ref

Reference whose mutability is inherited from the outer binding's origin. The closure doesn't pick read or mut; it forwards whatever the origin already carries:

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

# `xs` uses default `read` convention, immutable reference
def from_read(xs: List[Int]):
show_mutability(xs)

# `xs` uses `mut` convention, mutable reference
def from_mut(mut xs: List[Int]):
show_mutability(xs)

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

ref is the only convention that forwards origin information unchanged. read and mut create references with a fixed mutability; var removes the origin relationship entirely.

ref captures are intended for generic code that must operate across mutability contexts. In ordinary closures, read and mut produce clearer signatures.

Empty and omitted capture lists

{} and no capture list at all produce the same result: any reference to an outer value is rejected:

error: Could not infer capture convention of the captured
value a

Both forms allow a body that uses only its own arguments. The function then behaves as a plain nested function with no captures.

{} is preferred when the absence of captures is intentional; the explicit braces make the constraint visible at the declaration.

Mixing conventions

Each entry carries its own convention independently:

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 declaration time)

A bare name in a mixed list uses read, not the convention of its neighbors:

# y is captured as 'read', not 'mut'
def f() {var z, mut x, y}:
# ...

Default convention

A convention keyword without a name sets the default for every free variable not named explicitly:

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

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

mixed() # 11 22 snapshot
z = "changed"
mixed() # 21 42 snapshot
# ('z' was copied at declaration)

Rules:

  • At most one default-convention entry per list.
  • Position within the list isn't significant.
  • Trailing commas are accepted.
  • The default doesn't apply to names covered by an explicit entry. In {mut, var z}, the explicit var z overrides the default for z.

Parametric closures

A closure can declare its own compile-time parameter list:

def main():
# The `Intable` trait supports `Int` conversion
def double[T: Intable](x: T) {} -> Int:
return Int(x) * 2

print(double[Int](5)) # 10
print(double[Float64](3.4)) # 6

The parameter list, capture list, effects, and return type appear in the same order as on top-level functions: name[parameters](arguments) effects {captures} -> ReturnType.

Parameters and captures compose: the closure's parameters are bound at each call site, while the capture list controls its relationship to the enclosing scope. Variadic parameters are also legal (def closure[*Ts: Coord](*args: *Ts)).

Effects

Effects appear between the argument list and the capture list.

EffectForm
raises(args) raises {captures} -> T
register_passable(args) register_passable {captures} -> T

raises example:

def main() raises:
var y = 2

def divide(x: Int) raises {var y} -> Int:
if y == 0:
raise Error("divide by zero")
return x // y

print(divide(10)) # 5

register_passable example:

def main():
var base = 10

def shift(x: Int) register_passable {var base} -> Int:
return x + base

print(shift(3)) # 13

thin and abi("C") apply only to closure types used as function parameters, not to closure declarations. thin describes a non-capturing function type, which is incompatible with a closure that captures. See Function declarations.

Nesting

Closures can nest inside closures. Each level has its own capture list. A name captured at one level is visible to inner levels through their own capture lists:

def main():
var y = 4

def outer() {var y} -> Int:
def inner() {var y} -> Int:
return y
return inner() + y

print(outer()) # 8

An inner closure can capture an outer closure by name. This is how nested callbacks compose:

def main():
def make_adder(n: Int):
def add(x: Int) {var n} -> Int:
return x + n

def twice(x: Int) {var add} -> Int:
return add(add(x))

print(twice(5)) # ((5 + 3) + 3) = 11
# add(add(5)) = add(8) = 11

make_adder(3)

Closures are values and can be used in capture lists. An inner closure that names an outer closure must declare the same kind of capture (var, read, etc.) as it would for any other value.

Capture-list errors

Compiler complaintTrigger
Transfer sigil ^ without var convention^ after mut, read, or ref
Duplicate default conventionTwo bare convention keywords in one list
Unrecognized token in capture positionToken that isn't a convention keyword or name
Missing comma between entriesIdentifier followed by an unrecognized token
Unterminated capture listMissing closing }
Outer name not covered by capture listBody references an outer name the capture list doesn't cover
Copy or move capture of non-register-passable type in register_passable closure{var name} or {var name^} with a non-register-passable type
Use after move captureReference to a name after {var name^} consumed it

Restrictions

  • No escape. A closure can't outlive its enclosing scope. Returning a closure from its declaring function or storing it past the enclosing scope's end isn't supported.
  • No thin or abi("C") on declarations. These apply only to closure types used as function parameters; a declaration with captures can't be thin.
  • Trait conformance through captures. A struct can contain a closure-typed field and conform to a trait through it, but every method of that trait must be declared capturing until the capturing effect is removed (see unified_closure_structs.mojo).