Program Structure

Defer & Resources

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

Use defer for general scope-exit cleanup. For one owned File, using combines acquisition, use in a child scope, and cleanup through 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 aborts evaluation immediately. Lexical defer statements and using cleanup after that point are unspecified. A fault raised by a deferred action follows the runtime logging or collection policy. It is not a catchable value and leaves an Err already being returned unchanged.

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

Current Topaz resource behavior covers the File, defer, and using operations described above. User-defined disposal protocols, multi-resource acquisition, resource transfer, asynchronous cleanup, cancellation cleanup, host-exception cleanup, and module-lifetime finalization remain future decisions. Generated targets preserve the documented behavior or reject before artifact output. Check target-specific behavior on the intended target.

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.