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 follow a value through if, match, and for.
Make bad input an expected result
Create failures-resources.tpz:
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:
topaz check failures-resources.tpz
topaz run failures-resources.tpz
The invalid text is handled as data, so the process completes normally:
input: 25
ready: Build application, 25 minutes
inspection finished
input: later
cannot add task: minutes must be a positive integer
inspection finishedtoInt returns an Option: Some(value) means a parsed integer is present, while the other case covers absence. parseMinutes turns that absence, and nonpositive input, into a descriptive Result.
makeTask also returns Result. Postfix ? unwraps Ok and immediately returns a compatible Err, so no invalid task is constructed. inspect handles both cases explicitly.
The defer belongs to the scope that started the inspection. Its line appears after both the successful and failed input, even though the branches differ. In a resource-owning function, the deferred action is where cleanup belongs.
Inspect a broken contract
Now create intentional-fault.tpz:
let tasks = ["Run first program", "Build application"]
print(tasks[2])Running topaz run intentional-fault.tpz exits nonzero:
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 to recover from: the program violated the array’s bounds. The diagnostic identifies the contract and source span instead of inventing a value.
Decision: absence, recovery, or fault?
Use Option when no 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 reaches the guarded fallback and 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 say why bad text becomes an Err, why ? is safe inside makeTask, why the deferred line appears twice, and why an 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.