Program Structure

Defer & Resources

Choose between defer and the File-only using form, then evaluate cleanup order and the fault boundary.

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:

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 subsequent 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 under normal conditionsReturn 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 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.