Errors & Types

Lumina has no null and no exceptions. A value that can be missing has type T?, and an operation that can fail has type T!E. Both are in the type signature, match over them must be exhaustive, and short operators keep handling them cheap. Reading a field straight off a T? without unwrapping it is a compile error in progress, so you unwrap with ??, ?., match or if Some(x) = ....

Optional: T?

struct User {
    name: String
    age: Integer?
}

function findUser(id: Integer): User? {
    if id == 1 {
        return Some(User { name: "Ada", age: Some(36) })
    }
    return None
}

There are three ways to use an optional.

// 1. if-pattern: bind the value in the true branch only
if Some(user) = findUser(1) {
    age = user.age ?? 0              // 2. ?? gives a default
    console.println(`{user.name} is {age}`)
}

// 3. match, as an expression
name = match findUser(2) { Some(u) => u.name, None => "nobody" }

For nested optionals, ?. short-circuits: u.address?.city ?? "unknown".

Many standard operations return optionals instead of failing. For example, "42".toInteger() gives Some(42), and "abc".toInteger() gives None.

Result: T!E

Integer!ParseError reads as "an Integer, or a ParseError". You construct it with Ok(...) and Err(...). The error type is usually an enum, and variants can carry data:

enum ParseError {
    Empty
    NotANumber { text: String }
}

function parseAge(text: String): Integer!ParseError {
    if text.length() == 0 {
        return Err(ParseError.Empty)
    }
    return match text.toInteger() {
        Some(n) => Ok(n),
        None => Err(ParseError.NotANumber { text: text })
    }
}

? propagates

The ? operator unwraps an Ok, or returns the Err from the current function straight away:

function doubledAge(text: String): Integer!ParseError {
    age = parseAge(text)?
    return Ok(age * 2)
}

This is the same operator as Rust's, and it plays the role of Go's if err != nil { return err }, without the repetition. The error stays in the signature, so a caller can always see what can fail.

Handling at the edge

function describe(e: ParseError): String = match e {
    ParseError.Empty => "empty input",
    ParseError.NotANumber { text } => `not a number: {text}`
}

for input in ["21", "", "abc"] {
    match doubledAge(input) {
        Ok(v) => console.println(`ok {v}`),
        Err(e) => console.println(`error: {describe(e)}`)
    }
}
ok 42
error: empty input
error: not a number: abc

Enums and exhaustive match

Enums are tagged unions, as in Rust or Swift. match must cover every variant, so adding a variant breaks every match that forgot about it, at compile time:

enum Light {
    Red
    Yellow
    Green
}

function next(l: Light): Light = match l {
    Light.Red => Light.Green,
    Light.Green => Light.Yellow
}
error[E2011]: main.lumina:7:40: non-exhaustive match on `Light` — missing variants: Light.Yellow
  help: Add arms for all variants or use `_` wildcard

Patterns can destructure payloads, match literals, use guards, and fall back to _:

enum Shape {
    Circle { radius: Float }
    Rect { w: Float, h: Float }
}

function area(s: Shape): Float = match s {
    Shape.Circle { radius } => 3.14159 * radius * radius,
    Shape.Rect { w, h } => w * h
}

function sign(n: Integer): String = match n {
    0 => "zero",
    x if x < 0 => "negative",
    _ => "positive"
}

Union types and narrowing

A union String | Integer is a value of either type. To narrow it, write a typed binding in a match arm:

function show(v: String | Integer): String = match v {
    s: String => `text "{s}"`,
    n: Integer => `number {n}`
}

Inside the arm, s has type String. Watch out for one thing: a bare name such as String => is a binding that catches everything, not a type test. Always write name: Type => when you mean to narrow.

How this compares

ConceptTypeScriptGoRustLumina
Missing value`T \undefined`nil / zero valueOption<T>
Failurethrow(T, error)Result<T, E>T!E
Propagatetry/catchif err != nil??
Default??manual.unwrap_or()??
Exhaustive switchpartial (never trick)noyesyes

Gotchas

Next: Generics & Interfaces