Defer
defer schedules a statement to run when the current scope exits. It’s the cleanest way to handle cleanup — close files, free resources, release locks — without scattered goto cleanup or repeated code at every exit point.
Basic Defer
Anything after defer runs when the enclosing block ends:
fn main() {
print("start\n")
defer { print("cleanup\n") }
print("middle\n")
// cleanup runs here, as main exits
}
Output:
start
middle
cleanup
The defer body runs after the normal code in the block, on the way out.
LIFO Order
Multiple defers execute in reverse order — last declared, first executed:
fn main() {
defer { print("1\n") }
defer { print("2\n") }
defer { print("3\n") }
print("main done\n")
}
Output:
main done
3
2
1
This is like stacking cleanups — the most recent one runs first, so resources are released in the opposite order they were acquired.
Defer with Return
Defers run before a function returns, even if you return early:
fn process:bool(value:i32) {
defer { print("process exiting\n") }
if (value < 0) {
print("negative, abort\n")
return false // defer runs before returning
}
print("processing {value}\n")
return true // defer runs here too
}
fn main() {
process(10)
print("---\n")
process(-5)
}
Output:
processing 10
process exiting
---
negative, abort
process exiting
This means you can always count on cleanup happening, no matter which return path is taken.
Block Scope
Defer attaches to the nearest { } block, not just functions:
fn main() {
print("outer start\n")
{
defer { print("inner cleanup\n") }
print("inner body\n")
} // inner defer runs here
print("outer end\n")
}
Output:
outer start
inner body
inner cleanup
outer end
This lets you manage cleanup for arbitrary scopes, not just full functions.
Practical Example: Allocator Cleanup
The most common use: making sure resources you allocate get released:
fn main() {
var arr:array<i32>
arr.init()
defer { arr.free() } // guaranteed to run on any exit path
arr.push(10)
arr.push(20)
arr.push(30)
for (var v in arr) {
print("{v}\n")
}
// arr.free() runs here automatically
}
No matter how main exits — normal end, early return, or even a panic — arr.free() runs.
Defer in Loops
Defer statements inside a loop accumulate and run when the containing scope exits, not when each loop iteration ends:
fn main() {
for (i = 1:3) {
defer { print("defer {i}\n") }
print("loop {i}\n")
}
print("done\n")
}
Output:
loop 1
loop 2
loop 3
done
defer 3
defer 2
defer 1
If you want a defer to run per iteration, wrap the body in its own block:
fn main() {
for (i = 1:3) {
{
defer { print("cleanup {i}\n") }
print("work {i}\n")
}
}
}
Try it — Write a function that opens “resource A”, then “resource B”, then panics or returns early. Use two defers to release them and verify the order in the Playground.
Expert Corner
How defer is compiled: The GX compiler collects all defer statements per scope and emits them in reverse order before every exit point — normal fall-through, return, break, continue. There’s no runtime machinery, no extra allocations, no exceptions. The generated C code is exactly what you would have written by hand, just without the tedium.
Return-value safety: When a function has both defer and return <expr>, the compiler stores the return expression in a temporary first, runs the defers, then returns the temp. So defer { x = 999 } before return x returns the original value of x, not the post-defer one. This prevents the footgun where deferred cleanup silently corrupts your return value.
Why defer is better than goto cleanup: The C idiom goto cleanup; works but requires:
- A shared cleanup label at the bottom of the function
- All resources declared at the top
- Careful ordering of the goto targets
- No way to skip cleanup for variables not yet initialized
Defer solves all of these: cleanup is declared right where the resource is acquired, order is automatic (LIFO), and a defer only runs if execution actually passed through its declaration.
Comparing to Go and Zig: Go’s defer runs at function exit (not block exit). Zig’s defer runs at block exit (like GX). Block-scoped defer is more flexible — you can pair it with a { } block anywhere, not just inside a function. GX’s version matches Zig’s semantics.
Defer is NOT RAII: Unlike C++ destructors, defer is explicit and local. There’s no “type has a destructor that runs automatically” — you write defer { cleanup() } right next to the allocation. This keeps cleanup visible and under your control. The tradeoff: you must remember to write the defer. The benefit: no hidden behavior, no destructor reordering surprises, no RAII overhead.