Skip to content

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.

07-defer.zolo
Playground
// 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)

defer is a statement, not a keyword at the expression level. You can use defer as a variable name outside a statement context, but inside the statement the compiler recognises it as defer.

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.

10-defer-ok-err.zolo
Playground
// 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?

enespt-br