Use defer when a lexical scope must perform cleanup before exiting. 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.tpzThe 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 subsequent 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 under normal conditions | 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 defer statements in one scope run in last-in, first-out order. They also run when return, postfix ?, break, or continue exits 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 ? exits 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 defer statements or using cleanup will run 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 operations is not portable, so standard 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.