Skip to content

Defer

Register deterministic LIFO cleanup for normal and early scope exits.

Updated View as Markdown

defer expression schedules an expression for the exit of the current lexical scope.

handle := open_resource()?
defer handle.close()

use_resource(handle)?

The deferred expression is associated with the block containing the defer, not merely with the whole function.

Registration happens only when execution reaches the statement. A defer inside a selected branch is not registered when another branch runs.

if tracing_enabled {
    span := start_span()
    defer span.finish()
    execute()
}

Here finish belongs to the conditional branch’s block and runs before that branch leaves.

Exit paths

A defer runs when its scope exits normally and when control leaves through an explicit function return or propagated error.

fn process(name: string): int ! ResourceError {
    handle := open_resource(name)?
    defer close_resource(handle)

    data := read_resource(handle)?
    return data.len()
}

The cleanup runs after the return value has been evaluated but before the function completes. This lets the return expression read the resource it is about to release.

Loop transfers unwind the lexical scopes they leave before continuing at their target. A defer in an inner loop block therefore belongs to each iteration’s block exit.

for name in names {
    handle := open_resource(name)?
    defer close_resource(handle)
    process_resource(handle)?
}

Each iteration releases its own handle before the next iteration begins. A defer placed outside the loop would instead wait for the surrounding scope.

Hard runtime teardown and host-process failure are outside ordinary source control flow. Do not use defer as the only persistence guarantee for data that must survive process loss.

Ordering

Defers registered in one scope run in reverse order.

defer first_cleanup()
defer second_cleanup()
// second_cleanup, then first_cleanup

When several scopes are exited, the innermost scope is cleaned first.

defer outer_cleanup()
{
    defer inner_first()
    defer inner_second()
}
// inner_second, inner_first, then later outer_cleanup

This last-in, first-out order mirrors nested acquisition.

Evaluation

The deferred expression executes at scope exit. Names in it refer to the captured lexical places kept alive for that cleanup.

mut count := 0
defer println(count)
count = 3
// prints 3 at scope exit

If a specific value must be preserved independently of later mutation, bind a separate immutable snapshot and refer to it.

starting_count := count
defer report_start(starting_count)

Keep deferred expressions small. A named cleanup function is often clearer than a large inline block.

The values needed by a deferred expression remain live until it runs. Capturing a large buffer or resource can therefore intentionally extend its lifetime to the end of the scope; introduce a smaller snapshot when only a small piece of state is required.

Errors

Postfix ? is rejected anywhere inside a deferred expression. There is no caller continuation to which a cleanup failure can transparently propagate after the surrounding exit is already underway.

defer maybe_close()? // error

Handle a fallible cleanup explicitly.

defer match maybe_close() {
    Ok(_) => {}
    Err(error) => report_cleanup(error)
}

Choose a policy appropriate to the resource: log, record, retry safely, or ignore a documented idempotent close failure. Do not silently discard an important transactional error.

Suspension

A deferred call participates in ordinary effect inference. If it can suspend, the enclosing function and its callers must permit that effect, and the compiler preserves the deferred state across the suspension.

Prefer synchronous, idempotent cleanup where possible. Long-running work in a defer delays every exit path and makes cancellation behavior harder to reason about.

Destruction

Compiler-managed value destruction, reference counting, weak-reference maintenance, and user Drop dispatch do not require a source defer.

Use defer when the paired action is a semantic operation visible to the program:

  • close a host handle;
  • finish a tracing span;
  • restore temporary process or task state;
  • release a manually acquired external lease;
  • run a best-effort compensating action.

Use a transaction block for database commit/rollback semantics rather than reimplementing them with deferred SQL.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close