Error Handling
Error handling is an essential part of writing real-world programs. Luma’s error model follows the same philosophy as the rest of the language: clean, minimal, method-chainable, and beginner-friendly.
Luma’s error system has four concepts:
error("message")— raise a recoverable error.or()— handle errors inline, method-chain styletry— propagate errors to the callerpanic("message")— crash immediately, not recoverable
No try/catch blocks. No match statements. Just methods and one keyword.
Raising Errors with error(...)
Inside any function, you can raise a recoverable error:
fn divide(a: int, b: int) -> int {
if b == 0 {
error("Division by zero")
}
return a / b
}error("...") immediately stops the function. You do not write return error(...) — it’s a statement, like return.
Handling Errors with .or()
.or() is a method you can chain onto any expression. It catches errors and provides a fallback — just like Luma handles everything else: with a dot method.
Simple fallback value
x: int = divide(10, 0).or(0)
name: str = parse_name(input).or("unknown")If divide errors, x gets 0. If it succeeds, x gets the result. The .or() is a safety net.
Lambda with error access
x: int = divide(10, 0).or(err -> {
print("Failed: ${err}")
return 0
})The lambda receives the error message. You can log it, transform it, or return a custom fallback.
Expression lambda (short form)
x: int = divide(10, 0).or(err -> -1)Same rules as lambdas everywhere else in Luma — expression body or block body.
Chaining .or() in Pipelines
.or() catches any error that happened before it in the chain. This lets you control exactly where and how errors are handled:
Handle at each step (precise control)
content: str = file("config.json").read().or(err -> {
print("Can't read config: ${err}")
return "{}"
})
count: int = content.to_int().or(err -> {
print("Bad number: ${err}")
return 0
})Each .or() resolves the error at that step and returns a clean value. The chain continues safely.
Handle at the end (catch-all)
count: int = file("config.json").read().to_int().or(0)If .read() errors, .to_int() is skipped, and .or(0) catches whatever went wrong. If .read() succeeds but .to_int() errors, .or(0) catches that instead. Either way, count gets 0.
This is useful when you don’t care which step failed — you just want a safe default.
Mix both styles
content: str = file("config.json").read().or("0")
count: int = content.to_int().or(0)Handle the file error with a string fallback, then handle the conversion error separately.
Default Behavior (No .or(), No try)
If you call a function that errors and don’t handle it:
x: int = divide(10, 0)
print(x)The error crashes the program with a clear message. This is the safe default — errors never disappear silently. Beginners see immediately when something goes wrong, and the error message tells them where to add .or().
.or() on Optionals
.or() works on optionals too — same method, same mental model:
name: ?str = find_user(42)
safe: str = name.or("guest")Optionals use nil for expected absence. Errors use error() for unexpected failures. But the handling is the same: .or() gives you a fallback.
| Concept | Meaning | Handling |
|---|---|---|
Optional (?T) |
Value might be absent | .or(fallback) |
| Error | Something went wrong | .or(fallback) |
One method. Two use cases. No extra concepts to learn.
Propagating Errors with try
When you’re inside a function and don’t want to handle an error yourself, use try to pass it to your caller:
fn compute(a: str, b: str) -> int {
x: int = try parse_int(a)
y: int = try parse_int(b)
return try divide(x, y)
}try is shorthand for .or(err -> error(err)) — it re-raises the error. If the expression succeeds, you get the value. If it errors, your function errors with the same message.
The caller then decides what to do:
// Handle it
result: int = compute("10", "0").or(0)
// Or let it crash
result: int = compute("10", "0").or() is Always Safe
You can put .or() on any expression, even one that never errors:
x: int = add(2, 3).or(0)If add doesn’t error, .or() is a no-op — it compiles away. This means:
- You can use
.or()defensively - If a function starts erroring in the future, your code already handles it
- You don’t need to predict which functions might error
- The function’s documentation tells you what can go wrong, just like it tells you the parameter types
Unrecoverable Errors with panic(...)
Use panic for bugs or impossible situations:
panic("BUG: unreachable code")panic(...) crashes the program immediately. It cannot be caught with .or() or try. Use it for things that should never happen — not for errors users should handle.
Optional vs Error: When to Use Which
Use optionals (?T and nil) for expected absence:
fn find_user(id: int) -> ?str {
if id == 0 {
return nil
}
return "Alice"
}Use error() for unexpected failures:
fn divide(a: int, b: int) -> int {
if b == 0 {
error("Division by zero")
}
return a / b
}| Situation | Use | Why |
|---|---|---|
| “Not found” | return nil |
Absence is expected |
| “User skipped input” | return nil |
Absence is expected |
| “Division by zero” | error("...") |
Caller made a mistake |
| “File can’t be read” | error("...") |
Something broke |
| “Invalid format” | error("...") |
Bad input |
Summary
Luma’s error handling is built on the same principles as the rest of the language:
| Syntax | What it does |
|---|---|
error("msg") |
Raise an error inside a function |
.or(value) |
Fallback value if error (or nil) |
.or(err -> expr) |
Handle error with expression lambda |
.or(err -> { ... }) |
Handle error with block lambda |
try expr |
Propagate error to caller |
panic("msg") |
Crash immediately, not catchable |
No try/catch blocks. No match statements. No new syntax to learn. Just .or() — the same dot-method chaining Luma uses for everything else.
Future Enhancements
These features may be added later as Luma grows:
Custom Error Types
error MathError {
msg: str
code: int
}
fn divide(a: int, b: int) -> int {
if b == 0 {
error MathError("Division by zero", code: 400)
}
return a / b
}Error Context / Wrapping
content: str = try file("config.json").read().wrap("loading configuration")Distinguishing Error Types in .or()
result: int = compute(input).or(err -> {
if err.is_type("MathError") {
print("Math failed: ${err}")
return -1
}
return 0
})