Defer and Cleanup
defer registers cleanup work that runs when the nearest enclosing executable
invocation exits.
defer {
CloseTemporary()
}
A defer is registered only when execution reaches the statement. Once registered, it belongs to the complete function, method, accessor, initializer or lambda invocation rather than to the lexical block in which the statement appears.
Purpose
Use defer for cleanup that naturally belongs to the lifetime of the
current invocation but is not already expressed completely by ordinary ownership and
automatic destruction.
Common uses include:
- closing a temporary foreign resource;
- undoing temporary setup;
- releasing an API state entered earlier in the function;
- logging or accounting at invocation exit;
- cleanup during fallible construction;
- coordination with an external API that requires an explicit paired operation.
defer does not replace ownership, automatic destruction,
free or explicit error handling.
It composes with them.
Syntax
Sec 0.1 uses block-form defer:
defer {
statements
}
The body is always a block.
Shorthand forms are not part of Sec 0.1:
defer Close() // invalid
Write:
defer {
Close()
}
Registration Happens When Execution Reaches defer
Merely entering a lexical scope does not register its deferred work.
if connected {
defer {
Disconnect()
}
}
If connected is false, no deferred entry exists.
If execution reaches the defer, one cleanup entry is registered for the
nearest enclosing invocation.
The Execution Boundary Is the Enclosing Invocation
A registered defer runs when its nearest enclosing executable invocation exits.
It does not run merely because execution leaves an inner block, branch, loop or match arm.
Each invocation has its own defer-registration state.
Allowed Contexts
In Sec 0.1, defer is permitted inside:
- ordinary functions;
- instance methods;
- static functions and methods;
- lambdas;
- property getters;
- property setters;
init.
A defer always belongs to the nearest one of these executing boundaries.
defer Is Not Allowed Inside free
free is already lifecycle cleanup.
It may not register another invocation-scoped deferred cleanup.
free {
defer { // invalid
ReleaseSecondaryResource()
}
}
Locals created inside free still follow their ordinary automatic
destruction rules.
defer Is Not Block-Scoped Cleanup
fn Example(condition: bool) void {
if condition {
defer {
Log("leaving Example")
}
Work()
}
ContinueWork()
}
If the defer is registered, Log runs when Example exits,
not when the if body ends.
Leaving the branch does not create a cleanup boundary.
Deferred Entries Run in Reverse Registration Order
defer {
Log("first")
}
defer {
Log("second")
}
Exit order:
second
first
The ordering is defined by runtime registration order, not merely by source nesting.
Every execution of a defer statement creates one entry.
Deferred Cleanup and Automatic Destruction Share One LIFO Order
Sec does not first run all defers and then destroy all locals.
Cleanup obligations are ordered according to when those obligations are registered.
fn Example() void {
let first := OpenFirst()
defer {
Log("leaving")
}
let second := OpenSecond()
}
Relevant registration order:
destroy first
defer Log
destroy second
Exit order:
destroy second
run defer Log
destroy first
defer is part of the same cleanup model as
deterministic destruction, not a separate cleanup phase.
Earliest Legal Destruction Still Applies
Unified cleanup does not force every local value to remain alive until invocation exit.
A local may still be destroyed at its earliest legal lifetime end when no future ordinary or deferred use requires it.
A defer that references a value can extend that value's required lifetime:
fn Example() void {
if Ready() {
let resource := Open()
defer {
Use(resource)
}
}
ContinueWork()
}
If the defer was registered, resource must remain valid until the
deferred use executes.
A different local not needed by any later cleanup may still be destroyed earlier.
Deferred Statements Execute Later
The body of a defer is not evaluated when the defer is registered.
fn Example() void {
let mut value: int := 1
defer {
Print(value)
}
value = 2
}
The deferred block reads value when cleanup reaches the entry.
It therefore observes 2.
Registration establishes future uses of the referenced bindings; it does not snapshot their current values.
A Registered Defer Creates Real Future Uses
Ownership analysis treats a referenced value as needed by the later deferred block.
fn Example() void {
let resource := Open()
defer {
Use(resource)
}
Consume(<-resource) // invalid
}
Consuming resource would make the registered future use invalid.
The compiler must diagnose the conflict and identify the pending defer when practical.
The same principle applies to destruction, invalidation, replacement and other ownership-changing operations.
Borrowing Rules Still Apply
A deferred future use can keep a borrow or access requirement live until the defer executes.
The compiler therefore accounts for deferred:
- reads;
- writes;
- shared borrows;
- mutable borrows;
- resource uses;
- ownership dependencies.
A later operation that conflicts with the required future authority is rejected.
The compiler does not model the defer body as an arbitrary escaping callback. Its latest execution point is known: exit from the enclosing invocation.
Registration Is Path-Sensitive
if connected {
defer {
Disconnect()
}
}
Only paths that execute the defer acquire the cleanup obligation.
Flow analysis tracks that difference so the compiler neither invents cleanup on a path that never registered it nor forgets cleanup on a path that did.
defer Inside Loops
A loop does not create a defer-execution boundary.
for item in items {
defer {
Finish(item)
}
}
If the statement executes five times, five deferred entries are registered. All five run at invocation exit in reverse registration order.
This can be useful, but a long-running or unbounded loop may accumulate a large number of pending cleanups.
Analysis tooling may warn when immediate per-iteration cleanup is likely to be more appropriate.
Return Values Are Established Before Cleanup Runs
On ordinary return:
- the return expression is evaluated;
- the return result is established under ordinary ownership/move rules;
- required cleanup runs in canonical LIFO order;
- control returns to the caller.
fn Example() int {
let mut value: int := 1
defer {
value = 2
}
return value
}
The function returns 1.
The deferred mutation happens after the return value has already been established. A defer cannot retroactively replace that return value.
Error Results Are Also Established Before Cleanup
When a fallible invocation propagates an error, the propagated result is established before deferred cleanup begins.
Cleanup then executes in the same canonical order.
A defer cannot replace an already-established propagated error with a different propagated error.
Fallible Cleanup Must Be Handled Inside the Defer
A deferred block may call fallible operations, but the failure cannot propagate out into the already-exiting enclosing invocation.
Handle it locally:
defer {
match CloseResource() {
Ok() => {
}
Err(errorValue) => {
LogCloseError(errorValue)
}
}
}
Any ordinary Sec construct may be used when it fully handles the cleanup result.
A try may appear inside the defer only when its possible alternate
outcome is handled before the deferred block completes.
A try that would propagate out of the defer and change the enclosing
exit result is invalid.
Normal must-use and unused-result rules remain active.
defer does not introduce a special implicit discard rule.
return Cannot Target the Enclosing Invocation from a Defer
defer {
return // invalid
}
The enclosing return/error result has already been or will be established according to ordinary control flow.
Deferred cleanup cannot replace, restart or redirect that exit.
A function or lambda called by the deferred block may of course return normally from its own separate invocation.
break, continue and fallthrough Cannot Escape a Defer
These forms are invalid when they would target control flow outside the deferred block:
defer {
break // invalid
}
defer {
continue // invalid
}
defer {
fallthrough // invalid
}
Cleanup cannot resume or redirect control flow in the already-exiting invocation.
Nested defer Is Not Part of Sec 0.1
A deferred cleanup block may not register another defer:
defer {
defer { // invalid
CleanupAgain()
}
}
The cleanup schedule is determined by defer statements reached during ordinary execution, not by new registration while cleanup is already running.
Normal Completion of a Deferred Block
A defer completes by reaching the end of its block.
The block does not produce a value.
After it completes, cleanup proceeds to the next pending cleanup entry.
Panic Interaction
defer does not independently define panic or unwind policy.
Whether deferred cleanup runs during panic depends on the active panic-domain and target/profile rules described in Panic, Assertions and Runtime Checks.
When panic semantics require cleanup to run, deferred entries participate in the same canonical cleanup order as ordinary destruction.
When a root profile chooses immediate trap/reset/no-cleanup behavior, pending root defers are not guaranteed to run.
Required panic cleanup must itself satisfy the applicable no-panic rules.
defer Inside init
Fallible construction often needs temporary cleanup:
init(path: string) IOError {
let temporary := try OpenTemporary(path)
defer {
CloseTemporary(temporary)
}
BuildState(temporary)
}
The defer belongs to this initializer invocation.
Ownership analysis rejects a later transfer that would conflict with the pending deferred use.
If ownership of a temporary is intentionally transferred into the constructed object, the program must not leave a conflicting deferred cleanup that would release the same resource.
Property Accessors May Use defer
property Value: int {
get {
defer {
FinishRead()
}
return ReadValue()
}
}
The defer belongs to the current getter invocation.
The same rule applies to property setters, including fallible setters.
Ordinary accessor return, error, ownership and cleanup rules remain active.
Lambdas Have Their Own Defer Boundary
let work := fn() void {
defer {
FinishWork()
}
RunWork()
}
The defer runs when that lambda invocation exits, not when the function that created the lambda exits.
Calling the lambda several times creates independent defer registration state for every invocation.
Deferred Side Effects Occur at Cleanup Time
Statements in the block execute only when cleanup reaches the deferred entry.
Their observable effects therefore occur at cleanup time, not at registration time.
The compiler must not reorder defer registration across observable source operations when such reordering could change cleanup behavior.
Backend optimization is free to change implementation details only when the observable cleanup order stays identical.
Interaction with Automatic Destruction
Prefer ordinary ownership and automatic destruction when an owning Sec type already models the resource correctly.
A defer is useful when cleanup requires a different operation or external coordination.
Be careful not to release one resource twice:
let resource := OpenOwned()
defer {
ReleaseRawHandle(resource.Raw)
}
If resource would also automatically release the same underlying handle,
the design must resolve ownership explicitly rather than allowing both cleanup paths
to run independently.
If explicit deferred cleanup consumes or invalidates an owning value, the later automatic destruction obligation is updated through ordinary ownership/lifecycle semantics. Sec never silently performs a second release.
Deferred Uses Require Definite Assignment at Registration
A value read by a defer must already be definitely initialized on every path that reaches and registers that defer.
let value: int
defer {
Print(value) // invalid
}
Assigning value later does not retroactively make this registration valid.
The compiler validates the future use at the point where the defer is registered.
Name Resolution Uses the Declaration Site
Deferred execution happens later, but lexical name lookup does not move forward in time.
defer {
Use(resource) // invalid
}
let resource := Open()
resource is not visible at the defer declaration site.
Names in a defer body follow ordinary lexical scope rules.
Sec 0.1 Has No Special Defer Capture-by-Value Syntax
A defer does not snapshot referenced variables at registration time.
It follows ordinary future-use and borrowing rules for the referenced bindings and reads them when the defer executes.
If a programmer needs a stable earlier value, create an ordinary binding explicitly:
let snapshot := value
defer {
Print(snapshot)
}
value = nextValue
A Defer Body Is Not an Arbitrary Escaping Callback
The compiler knows exactly where a registered defer may execute: at cleanup of the enclosing invocation.
This lets ownership, borrowing and lifetime analysis model deferred future uses more precisely than an unknown callback that may escape indefinitely.
Semantic lowering may use cleanup blocks or another implementation technique, but source semantics remain explicit deferred cleanup.
Diagnostics
Defer diagnostics should point to both the invalid operation and the relevant registered defer where useful.
Defer inside free
defer is not allowed inside `free`
`free` is already lifecycle cleanup
help: perform the required cleanup directly in the free body
Move conflicts with a pending deferred use
cannot move `resource`
`resource` is used by a registered defer:
defer {
Use(resource)
}
help: move the value before registering the defer
help: remove or restructure the deferred future use
Not definitely initialized
`value` is not definitely initialized when this defer is registered
the deferred block may read `value` when the invocation exits
help: initialize the value before the defer statement
Propagation from deferred cleanup
error propagation cannot leave a deferred cleanup block
the enclosing invocation is already exiting
help: handle the cleanup failure inside the defer
help: log, translate, or explicitly discard the result when permitted
Nested defer
nested defer is not part of Sec 0.1
cleanup code cannot register new deferred cleanup while cleanup is already running
Escaping control transfer
return is not allowed inside a deferred cleanup block
break cannot target an enclosing loop from inside a deferred cleanup block
continue cannot target an enclosing loop from inside a deferred cleanup block
fallthrough is not allowed inside a deferred cleanup block
Formatter, LSP and Analysis
The formatter preserves canonical block syntax:
defer {
Cleanup()
}
It must not rewrite a defer into unsupported shorthand syntax.
LSP and analysis may expose:
- the invocation boundary that owns a defer;
- registration point;
- possible registration paths;
- cleanup order relative to other defers and destruction;
- values whose lifetimes are extended by a deferred future use;
- moves or mutations blocked by a pending defer;
- defer accumulation in loops;
- fallible cleanup that is not fully handled;
- panic/no-panic effects reachable from the defer body.
Analysis should avoid describing defer as an arbitrary escaping closure when its invocation-bounded lifetime provides stronger information.
Best Practices
- Prefer automatic ownership and destruction when the resource already has a correct owning Sec type.
- Use
deferfor explicit paired cleanup or invocation-exit side effects. - Register cleanup immediately after the operation that makes the cleanup necessary.
- Keep deferred blocks short and cleanup-focused.
- Remember that a defer runs at invocation exit, not block exit.
- Do not move or invalidate values that a registered defer will need later.
- Use explicit ordinary bindings when you need a snapshot of an earlier value.
- Handle fallible cleanup completely inside the defer.
- Do not try to change an already-established return or propagated error from a defer.
- Avoid accumulating large numbers of defers in long-running loops when per-iteration cleanup is more appropriate.
- Do not use defer to simulate general control flow.
- Check that deferred foreign cleanup and automatic Sec destruction cannot release the same resource twice.
Summary
deferregisters cleanup for the nearest enclosing executable invocation.- Sec 0.1 uses only block-form
defer { ... }. defer Call()shorthand is not part of Sec 0.1.- A defer is registered only when execution reaches the defer statement.
- Registration is path-sensitive.
- One defer statement may register multiple cleanup entries when executed repeatedly.
- Functions, methods, static functions, lambdas, property accessors and
initmay contain defer. freemay not contain defer.- A defer belongs to the invocation, not the lexical block where it was registered.
- Leaving an if/match/loop block does not execute its registered defer.
- Deferred entries run in reverse runtime registration order.
- Deferred cleanup and automatic destruction use one unified LIFO cleanup model.
- Sec does not have a separate “all defers first” cleanup phase.
- Ordinary values may still be destroyed at their earliest legal lifetime end.
- A deferred future use may extend a value's required lifetime.
- The defer body is not evaluated at registration time.
- Ordinary variable reads in a defer observe the value when cleanup executes.
- Sec 0.1 has no special defer capture-by-value syntax.
- Registering a defer creates real future ownership and borrow uses.
- A value cannot be moved, destroyed or invalidated when that would break a pending deferred use.
- Borrowing rules remain fully active for deferred future uses.
- The compiler knows that a defer cannot outlive the enclosing invocation.
- A defer inside a loop registers one entry each time execution reaches it.
- A loop does not create a defer-execution boundary.
- Analysis may warn about unexpectedly large defer accumulation in loops.
- On return, the return expression is evaluated and the return value established before cleanup runs.
- A deferred mutation cannot retroactively change an already-established return value.
- On propagated error, the propagated result is established before deferred cleanup begins.
- A defer cannot replace the already-established propagated error.
- Fallible cleanup must be handled inside the deferred block.
- A try inside defer may not propagate out into the already-exiting invocation.
- Normal Result must-use and explicit-discard rules remain active inside defer.
returnmay not target the enclosing invocation from inside a defer.break,continueandfallthroughmay not escape a deferred cleanup block.- Nested defer is not part of Sec 0.1.
- A defer completes by reaching the end of its block and produces no value.
- Panic behavior does not come from defer itself.
- When the panic policy requires cleanup, defers participate in the ordinary canonical cleanup order.
- A panic profile that chooses immediate root termination may provide no guarantee that root defers run.
- A defer in
initbelongs to that initializer invocation. - Temporary construction cleanup must not conflict with ownership transferred into the completed object.
- Property getters and setters may register defer cleanup.
- A lambda's defer belongs to the lambda invocation, not to the function that created the lambda.
- Each lambda call has independent defer-registration state.
- Deferred side effects happen at cleanup time.
- The compiler may optimize defer implementation only when observable cleanup ordering is unchanged.
- Explicit deferred cleanup must not double-release a resource also owned by automatic destruction.
- A value read by defer must be definitely initialized when the defer is registered.
- Assigning a value later does not retroactively make an earlier deferred future use valid.
- Names inside defer use ordinary lexical name resolution at the declaration site.
- A declaration written after the defer is not visible merely because the defer executes later.
- Defer is an explicit cleanup construct, not an arbitrary escaping callback.
- Diagnostics should identify pending deferred uses when they block move, mutation, destruction or invalidation.