Functions can declare preconditions and postconditions, checked at runtime on every call. A failed contract throws ContractViolation.
def divide(a: Int, b: Int) -> Int {
in {
b != 0
}
return a // b
}
def abs_val(x: Int) -> Int {
out (r) {
r >= 0
}
if x < 0 {
return -x
}
return x
}
in {} checks conditions before the body runs. out(r) {} checks the return value. old(expr) captures a value at function entry for comparison in postconditions. Classes and data types can declare invariant {} blocks. See Contracts for the full reference.
def divide(a: Int, b: Int) -> Int {
in {
b != 0
}
return a // b
}
try {
let result = divide(10, 0)
} except ValueError as e {
print("Bad value")
} except Error as e {
print("Some other error")
} finally {
print("Always runs")
}
raise ValueError("msg") throws an error. raise "msg" wraps in RuntimeError. Built-in error types: Error, RuntimeError, TypeError, ValueError, IOError, KeyError, IndexError, ContractViolation.
Result[T, E] represents success or failure without exceptions:
def safe_divide(a: Int, b: Int) -> Result[Int, Str] {
if b == 0 {
return Err("division by zero")
}
return Ok(a // b)
}
let r = safe_divide(10, 3)
match r {
case Ok(value) {
print(f"got {value}")
}
case Err(msg) {
print(f"failed: {msg}")
}
}
The ? postfix operator unwraps Ok or returns Err early from the enclosing function. Use ?else return for early return with a default value when unwrapping fails:
def get_name(id: Int) -> Str {
let user = find_user(id) ?else return "unknown"
return user.name
}
See Error handling for the full reference.
defer schedules a block to run when the enclosing scope exits, regardless of how it exits (normal return, exception, or early return). Deferred blocks run in reverse order of declaration:
def process_file(path: Str) -> None {
let f = open(path)
defer { close(f) }
# work with f... cleanup happens automatically
}