Skip to content

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.

06-try-catch.zolo
Playground
// 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 e captured by catch contains the panic message prefixed with the code location (file and line) — it is not just the string you passed to panic.

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.

08-panic-and-catch.zolo
Playground
// 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>.

enespt-br