defer, defer_ok and defer_err
defer schedules a statement to run when the current scope ends — whether
by a normal return, a panic, or any other path. Multiple defer statements in
the same function execute in LIFO order (stack): the last one registered
runs first.
LIFO, practical resource cleanup, defer on panic and inner block scope.
// Feature: defer — schedule cleanup at the end of the block
// Syntax: `defer <stmt>`
// When to use: release a resource, close a file, log on exit.
// -- defer runs on EXIT of the function, in LIFO order --------
fn lifo_order() {
defer print("a") // last to register, first to run? NO
defer print("b")
defer print("c")
print("body")
}
lifo_order()
// expected:
// body
// c
// b
// a
// (the most recently registered defer runs first — LIFO / stack)
// -- Practical case: ensure cleanup ---------------------------
fn process() {
print("opening file")
defer print("closing file") // ensures closing
print("reading data")
print("processing")
}
process()
// expected:
// opening file
// reading data
// processing
// closing file
// -- defer on a normal return ---------------------------------
fn maybe_fail(fail: bool) {
defer print("defer ran")
if fail {
panic("oops")
}
print("no panic")
}
try { maybe_fail(false) } catch e { print("caught") }
// expected:
// no panic
// defer ran
try { maybe_fail(true) } catch e { print("caught") }
// expected:
// defer ran
// caught
// (defer fires on panic too — per the spec exit table, panic counts
// as an error exit and the function-level pcall recovers it before
// the drain runs.)
// -- defer respects block scope -------------------------------
fn block_scope() {
print("start")
{
defer print("inner")
print("inside the block")
}
print("end")
}
block_scope()
// expected:
// start
// inside the block
// inner
// end
// (defer runs on EXIT of the block where it was declared)
deferis a statement, not a keyword at the expression level. You can usedeferas a variable name outside a statement context, but inside the statement the compiler recognises it asdefer.
When the return type is Result<T, E>, you can be more precise:
defer_ok runs only when the function returns Ok; defer_err runs only
when it returns Err. All three keywords (defer, defer_ok, defer_err)
share the same LIFO stack:
defer/defer_ok/defer_err trio, bindings that receive the return value, rollback/commit pattern.
// Feature: `defer_ok` / `defer_err` — cleanup filtered by exit path.
// Syntax: `defer_ok [|v[: T]|] <expr>` / `defer_err [|e[: E]|] <expr>`
// When to use: differentiate cleanup that runs only on success
// (commit, metrics.success, log.info) from cleanup that runs only
// on failure (rollback, metrics.failure, alarms). Shares the same
// LIFO stack as plain `defer`. See `specs/defer-ok-err.md`.
use std::Result
// -- Triad ordering — single LIFO across the three keywords ----
fn op_ok() -> Result<int, str> {
defer print("A always")
defer_err print("B err — should NOT print")
defer_ok print("C ok")
defer print("D always")
return Result.Ok(42)
}
print("--- op_ok ---")
let _ = op_ok()
// expected:
// --- op_ok ---
// D always
// C ok
// A always
// (B is filtered because the exit value is Ok)
fn op_err() -> Result<int, str> {
defer print("A always")
defer_err print("B err")
defer_ok print("C ok — should NOT print")
defer print("D always")
return Result.Err("oops")
}
print("--- op_err ---")
let _ = op_err()
// expected:
// --- op_err ---
// D always
// B err
// A always
// -- Bindings — receive the return value / error --------------
fn compute() -> Result<int, str> {
defer_ok |v| print("ok with v")
defer_err |e| print("err with e")
return Result.Ok(100)
}
print("--- compute (ok) ---")
let _ = compute()
// expected:
// --- compute (ok) ---
// ok with v
// -- Typed binding ---------------------------------------------
fn typed_log() -> Result<int, str> {
defer_err |e: str| print("typed err")
return Result.Err("boom")
}
print("--- typed_log ---")
let _ = typed_log()
// expected:
// --- typed_log ---
// typed err
// -- Rollback pattern without a flag --------------------------
fn transfer(commit_ok: bool) -> Result<int, str> {
print("begin tx")
defer_err print("rollback tx")
defer_ok print("commit tx")
if !commit_ok {
return Result.Err("validation failed")
}
return Result.Ok(1)
}
print("--- transfer(true) ---")
let _ = transfer(true)
// expected:
// --- transfer(true) ---
// begin tx
// commit tx
print("--- transfer(false) ---")
let _ = transfer(false)
// expected:
// --- transfer(false) ---
// begin tx
// rollback tx
The classic transaction pattern becomes natural with defer_ok / defer_err:
register defer_err print("rollback") right after opening the transaction, and
defer_ok print("commit") right after. The happy path commits; any
return Err(...) before the end triggers the rollback — no boolean flag needed.
Challenge
In the defer example, add a second inner block with two defer statements and
observe the execution order. What happens to the outer defers?
See also