Learn Topaz

Failures and Resources

Handle expected invalid input with Option, Result, and postfix ?, guarantee cleanup with defer, and inspect an intentional fault.

Outcome: Represent expected absence and recoverable failure in values, guarantee scoped cleanup, and distinguish both from a runtime contract fault.

Prerequisite: Complete Data and Control and be able to trace a value through if, match, and for.

Make bad input an expected result

Create failures-resources.tpz:

TOPAZ
record StudyTask {
    title: string,
    minutes: int,
}

function parseMinutes(text: string) -> Result<int, string> {
    match toInt(text) {
        case Some(value) if value > 0 => Ok(value)
        case _ => Err("minutes must be a positive integer")
    }
}

function makeTask(title: string, minuteText: string) -> Result<StudyTask, string> {
    let minutes = parseMinutes(minuteText)?
    Ok(StudyTask { title: title, minutes: minutes })
}

function inspect(minuteText: string) -> () {
    print("input: {minuteText}")
    defer { print("inspection finished") }
    match makeTask("Build application", minuteText) {
        case Ok(task) => print("ready: {task.title}, {task.minutes} minutes")
        case Err(message) => print("cannot add task: {message}")
    }
}

inspect("25")
inspect("later")

Check and run it:

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

The invalid text is handled as data, so the process completes normally:

Output
input: 25
ready: Build application, 25 minutes
inspection finished
input: later
cannot add task: minutes must be a positive integer
inspection finished

toInt returns an Option. Some(value) indicates that a parsed integer is present, while the alternative case covers absence. parseMinutes converts that absence and any non-positive input into a descriptive Result.

makeTask also returns a Result. Postfix ? unwraps Ok and immediately returns a compatible Err, so no invalid task is constructed. inspect handles both cases explicitly.

The defer statement belongs to the scope that started the inspection. Its line appears after both successful and failed input, even though execution follows different branches. In a resource-owning function, deferred actions are where cleanup belongs.

Inspect a broken contract

Now create intentional-fault.tpz:

TOPAZ
let tasks = ["Run first program", "Build application"]
print(tasks[2])

Running topaz run intentional-fault.tpz exits nonzero:

Output
error[TPZ4001]: index 2 is out of bounds for an array of length 2
 --> intentional-fault.tpz:2:7
  |
2 | print(tasks[2])
  |       ^^^^^^^^

This is a fault, not an Err intended for recovery. The program violated array bounds. The diagnostic identifies the contract and source span instead of inventing a value.

Decision: absence, recovery, or fault?

Use Option when the absence of a value is an ordinary outcome. Use Result when the caller can respond to a meaningful error, and use ? to propagate that error through another compatible Result. Use fault for a broken contract. Use defer independently whenever a scope must perform cleanup on every exit.

Try this

Add inspect("0") to the valid program. Which path runs, and does the deferred line still appear?

Answer

Zero fails the value > 0 guard, so the fallback branch prints cannot add task: minutes must be a positive integer. The next line is still inspection finished, because defer runs when the inspect scope exits.

Ready to continue when

You can explain why invalid text becomes an Err, why ? is valid inside makeTask, why the deferred line appears twice, and why out-of-bounds access is a fault rather than expected input.

See Null, Option, Result & Faults and Defer & Resources for exact rules. Continue to First Application.