panic, try/catch and catch_panic
panic is for bugs and broken contracts — situations that should never
happen if the code is correct. Unlike Result, a panic stops execution
immediately; it is not a value to propagate.
try / catch / finally lets you capture panics at a controlled boundary.
finally runs always, with or without a panic:
try/catch/finally, try as an expression, panic with interpolation, nested try.
// Feature: try / catch / finally
// Syntax: `try { ... } catch e { ... } finally { ... }`
// When to use: catch panics and ensure cleanup.
// -- Basic try / catch ----------------------------------------
try {
panic("something went wrong!")
} catch e {
print("caught!")
}
// expected: caught!
// (the variable `e` contains the message with the panic location info)
// -- try / catch / finally ------------------------------------
try {
print("trying...")
panic("oops")
} catch e {
print("caught error")
} finally {
print("cleanup always runs")
}
// expected:
// trying...
// caught error
// cleanup always runs
// finally runs even when there is NO panic.
try {
print("ok")
} catch e {
print("won't enter")
} finally {
print("still runs")
}
// expected:
// ok
// still runs
// -- try as expression — returns a value ----------------------
let v = try { 42 } catch e { -1 }
print(v) // 42
let f = try { panic("x"); 0 } catch e { 99 }
print(f) // 99
// -- panic with interpolated message --------------------------
fn divide(a: int, b: int) -> int {
if b == 0 {
panic("Division by zero: {a} / {b}")
}
return a / b
}
let safe = try { divide(10, 0) } catch e { -1 }
print(safe) // -1
print(try { divide(9, 3) } catch e { -1 })
// expected: 3 (exact division)
// -- Nested try/catch -----------------------------------------
try {
try {
panic("inner")
} catch e {
print("inner caught")
panic("re-thrown")
}
} catch e {
print("outer caught")
}
// expected:
// inner caught
// outer caught
The variable
ecaptured bycatchcontains the panic message prefixed with the code location (file and line) — it is not just the string you passed topanic.
When you need to treat a function that may panic as if it were Result, use
catch_panic. It returns an object with .ok (bool) and .value (the return
on success). To pass arguments, wrap the call in a closure:
panic, catch_panic with and without a closure, Result.from_pcall to convert panic into Result.
// Feature: panic and catch_panic
// Syntax: `panic("msg")`, `catch_panic(fn, args...)`
// When to use: non-recoverable errors (panic), safe wrapping (catch_panic).
use std::Result
// -- panic — interrupts execution ------------------------------
// When an invariant is violated and there's no reasonable recovery:
fn must_be_positive(n: int) -> int {
if n < 0 {
panic("n must be positive, got {n}")
}
return n
}
print(must_be_positive(5)) // 5
// Without catching, panic terminates the program. To catch:
let safe = try { must_be_positive(-3) } catch e { -1 }
print(safe) // -1
// -- catch_panic — wraps a function call -----------------------
// Useful when you want to call an external function that may
// panic and treat the result as data.
fn risky() -> int {
panic("boom")
return 0
}
let res = catch_panic(risky)
if res.ok {
print("value: {res.value}")
} else {
print("caught panic")
}
// expected: caught panic
// Success:
fn safe_fn() -> int {
return 42
}
let r2 = catch_panic(safe_fn)
if r2.ok {
print("value: {r2.value}")
} else {
print("caught panic")
}
// expected: value: 42
// -- catch_panic with a closure capturing arguments -----------
// To pass arguments to a function under catch_panic, wrap it in
// a zero-parameter closure.
fn divide_panic(a: int, b: int) -> int {
if b == 0 {
panic("division by zero")
}
return a / b
}
let ok = catch_panic(|| divide_panic(10, 2))
let err = catch_panic(|| divide_panic(10, 0))
print(ok.ok) // true
print(ok.value) // 5
print(err.ok) // false
// -- Result.from_pcall — converts panic into Result -----------
fn require_positive(n: int) -> int {
if n < 0 {
panic("negative")
}
return n * 2
}
fn describe(r: Result<int, str>) {
match r {
Result::Ok(v) => print("ok: {v}"),
Result::Err(_) => print("error caught"),
}
}
describe(Result::from_pcall(require_positive, 5))
// expected: ok: 10
describe(Result::from_pcall(require_positive, -1))
// expected: error caught
Practical rule: capture panics only at boundaries (application top level, tests, external library wrappers). Inside business logic, prefer
Result<T, E>.