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 style
  • try — propagate errors to the caller
  • panic("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
})
Last updated on