Error Handling

BioLang uses try/catch for exception-style error handling and assert(condition, "message") for precondition checks. Uncaught errors propagate up the call stack automatically.

try / catch

Wrap fallible code in try { ... } catch e { ... }. The caught value e is a string containing the error message:

# Catch a division-by-zero error
try {
  let x = 1 / 0
} catch e {
  println(f"Caught: {e}")
}

println("Execution continues after the catch block")

Error Types

Any runtime error can be caught: type errors, arithmetic errors, index-out-of-bounds, and I/O failures. The error message string describes the problem:

# Type error
try {
  let x = "hello" + 42
} catch e {
  println(f"Type error: {e}")
}

# Index out of bounds
try {
  let xs = [1, 2, 3]
  let bad = xs[10]
} catch e {
  println(f"Index error: {e}")
}

try as an Expression

try/catch is an expression — it returns the value of whichever branch executes:

# Provide a fallback value on failure
let result = try { 100 / 0 } catch _ { -1 }
println(f"Result: {result}")

# Fault-tolerant batch processing
let paths = ["good.csv", "missing.csv", "also_good.csv"]
let loaded = paths
  |> map(|p| try { read_csv(p) } catch _ { nil })
  |> filter(|x| x != nil)

assert(condition, message)

assert takes two arguments: a boolean condition and an error message string. If the condition is false, the assertion throws an error that can be caught with try/catch:

# Assertions that pass do nothing
assert(1 + 1 == 2, "math works")
assert(len([1, 2, 3]) == 3, "len is correct")
println("all assertions passed")

# A failing assertion throws an error
try {
  assert(1 > 2, "one is not greater than two")
} catch e {
  println(f"Caught assertion: {e}")
}

Error Propagation in Pipelines

Errors propagate automatically through pipe chains. If any step throws, the entire chain stops and the error bubbles up to the nearest try/catch:

# An error in any pipe stage stops the chain
fn must_be_positive(n) {
  if n <= 0 {
    error(f"expected positive, got {n}")
  }
  n
}

try {
  let result = [-1, 2, 3]
    |> map(must_be_positive)
    |> sum()
  println(f"Sum: {result}")
} catch e {
  println(f"Pipeline failed: {e}")
}

Per-Item Error Handling

To skip failures rather than aborting the whole pipeline, catch errors inside the map callback:

# Process each item, collect results and errors separately
let items = [10, 0, 5, 0, 20]

let results = items |> map(|x| {
  try {
    {value: 100 / x, ok: true}
  } catch e {
    {value: 0, ok: false}
  }
})

let good = results |> filter(|r| r.ok) |> len()
let bad = results |> filter(|r| !r.ok) |> len()
println(f"Succeeded: {good}, Failed: {bad}")

Validation Pattern

# Use assert for precondition checks
fn analyze(scores) {
  assert(len(scores) > 0, "scores list must not be empty")
  let avg = sum(scores) / len(scores)
  let mx = max(scores)
  {average: avg, maximum: mx}
}

# Valid call
let r = analyze([85, 92, 78])
println(f"Average: {r.average}, Max: {r.maximum}")

# Invalid call — caught by try/catch
try {
  analyze([])
} catch e {
  println(f"Validation error: {e}")
}