Use defer when a lexical scope must run cleanup before it exits. When that
scope owns exactly one File, the narrower using statement combines
acquisition, a child scope, and an implicit close.
Watch the cleanup order
Place a text file named defer-resources.txt beside the program. Its exact
contents are:
readySave this as defer-resources.tpz:
function inspectNote(path: string) -> Result<(), string> {
using file = open(path)? {
defer { print("body cleanup") }
let note = file.read()?
print("note scalars: {note.scalars().length}")
}
print("resource scope closed")
Ok(())
}
match inspectNote("defer-resources.txt") {
case Ok(_) => print("done")
case Err(message) => print("error: {message}")
}Check and run it:
topaz check defer-resources.tpz
topaz run defer-resources.tpz
The observed order is:
note scalars: 6
body cleanup
resource scope closed
doneopen(path)? acquires the file once. Only after acquisition succeeds does
using register its implicit close. The later body defer therefore runs
first, and the implicit close runs before execution reaches
resource scope closed.
Choose the form that owns the lifetime
| Situation | Use |
|---|---|
| One cleanup call or block belongs to the current scope | defer call or defer { ... } |
One acquired File belongs to one child block | using file = open(path)? { ... } |
| Acquisition, reading, or writing can fail normally | Return or match Result; use ? only in a compatible function |
| The program has violated a runtime contract | Treat it as a fault, not as recoverable cleanup control |
Several defers in one scope run last-in, first-out. They also run when
return, postfix ?, break, or continue crosses that scope. A using
binding is immutable and visible only inside its body. A closure may retain
the File value, but the implicit close still occurs when the using scope
ends, so retaining it does not extend the resource lifetime.
Result propagation is not a runtime fault
If open returns Err, no cleanup has been registered yet. If file.read()
returns Err, postfix ? crosses the active using scope, so its registered
cleanup runs before the error leaves inspectNote.
A runtime fault has a deliberately narrower promise: it aborts evaluation,
and the language does not guarantee that lexical defers or using cleanup
will drain afterward. A fault raised by a deferred action is handled by
runtime logging or collection policy; it does not become a catchable value or
replace an Err already being returned.
Common correction
Do not spell the File operation as File.open(...), and do not invent a
general using protocol. The exact acquisition function is open(path), and
the exact statement accepts one bare immutable name and one File.
Explicitly calling close() inside a using body does not cancel the
registered close attempt. Behavior after close or repeated close is not
portable, so normal code should let one owner close the resource once.
Exact boundary
User-defined disposable protocols, multi-resource acquisition, resource transfer, asynchronous cleanup, cancellation cleanup, host-exception guarantees, and module-lifetime finalization are not current Topaz behavior. Generated targets must preserve the stated behavior or decline the program; this interpreter transcript is not a blanket backend-parity claim.
Review the guided failure model in Failures and Resources, compare absence and recoverable errors in Null, Option, Result & Faults, and see the exact File operations in Files & Resources.