Program Structure

Defer & Resources

Choose defer or the File-only using form, then reason about cleanup order and the fault boundary.

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:

ready

Save this as defer-resources.tpz:

TOPAZ
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:

BASH
topaz check defer-resources.tpz
topaz run defer-resources.tpz

The observed order is:

Output
note scalars: 6
body cleanup
resource scope closed
done

open(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

SituationUse
One cleanup call or block belongs to the current scopedefer call or defer { ... }
One acquired File belongs to one child blockusing file = open(path)? { ... }
Acquisition, reading, or writing can fail normallyReturn or match Result; use ? only in a compatible function
The program has violated a runtime contractTreat 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.