← Back to Blog

Error Handling in Lateralus

August 5, 2025 languageerrors

Lateralus uses Result[T, E] for recoverable errors and panics for unrecoverable ones. The ? operator propagates errors through pipelines naturally. This post covers the error handling philosophy and practical patterns.

◉ The Problem with Exceptions

Exception-based languages (Java, Python, C++) have hidden control flow:

// Java: any of these might throw, but nothing tells you
String content = readFile(path);
Data data = parse(content);
Result result = process(data);

You can't tell from the code what errors are possible. The compiler doesn't help. Errors are invisible until runtime.

◉ Result Type

Lateralus makes errors explicit with Result[T, E]:

enum Result[T, E] {
    Ok(T),
    Err(E),
}

fn read_file(path: str) -> Result[str, IOError] {
    // Returns Ok(content) or Err(IOError)
}

fn parse(content: str) -> Result[Data, ParseError] {
    // Returns Ok(data) or Err(ParseError)
}

The return type tells you exactly what can go wrong. The compiler forces you to handle it.

◉ The ? Operator

Handling errors explicitly can be verbose:

fn process_file(path: str) -> Result[Data, Error] {
    let content = match read_file(path) {
        Ok(c) => c,
        Err(e) => return Err(e.into()),
    }

    let data = match parse(content) {
        Ok(d) => d,
        Err(e) => return Err(e.into()),
    }

    Ok(transform(data))
}

The ? operator eliminates boilerplate:

fn process_file(path: str) -> Result[Data, Error] {
    let content = read_file(path)?
    let data = parse(content)?
    Ok(transform(data))
}

// Or with pipelines:
fn process_file(path: str) -> Result[Data, Error] {
    read_file(path)?
        |> parse()?
        |> transform()
        |> Ok
}

The ? operator:

  1. If Ok(v), unwraps to v
  2. If Err(e), returns Err(e.into()) from the function

◉ Error Context

When errors bubble up, context is lost. Where did it fail? The .context() method adds information:

fn load_config(path: str) -> Result[Config, Error] {
    let content = read_file(path)
        .context("reading config file")?

    let parsed = parse_toml(content)
        .context("parsing config TOML")?

    let config = validate_config(parsed)
        .context("validating config")?

    Ok(config)
}

// Error message:
// Error: validating config
//   Caused by: missing required field 'database.url'
//   While: parsing config TOML
//   While: reading config file "/etc/app/config.toml"

◉ Error Types

Define domain-specific error types:

enum NetworkError {
    Timeout { host: str, after_ms: int },
    ConnectionRefused { host: str, port: int },
    DNSError { name: str },
    TLSError { reason: str },
}

impl NetworkError {
    fn message(self) -> str {
        match self {
            Timeout { host, after_ms } =>
                "Connection to " + host + " timed out after " + str(after_ms) + "ms",
            ConnectionRefused { host, port } =>
                "Connection refused to " + host + ":" + str(port),
            // ...
        }
    }
}

◉ Error Conversion

Convert between error types with From trait:

impl From[IOError] for AppError {
    fn from(e: IOError) -> AppError {
        AppError::IO(e)
    }
}

impl From[ParseError] for AppError {
    fn from(e: ParseError) -> AppError {
        AppError::Parse(e)
    }
}

// Now ? automatically converts:
fn load_data(path: str) -> Result[Data, AppError] {
    let content = read_file(path)?  // IOError -> AppError
    let data = parse(content)?      // ParseError -> AppError
    Ok(data)
}

◉ Option Type

For "might not exist" without errors:

enum Option[T] {
    Some(T),
    None,
}

fn find_user(id: int) -> Option[User] {
    // Returns Some(user) or None
}

// Use with match
match find_user(123) {
    Some(user) => println("Found: " + user.name),
    None => println("User not found"),
}

// Or with ? in Option-returning functions
fn get_user_email(id: int) -> Option[str] {
    let user = find_user(id)?
    user.email  // Might also be Option
}

◉ Combining Results and Options

// Convert Option to Result
let user = find_user(123)
    .ok_or(Error::UserNotFound(123))?

// Convert Result to Option (discarding error)
let user = load_user(123).ok()

◉ Panics

For unrecoverable errors:

fn divide(a: int, b: int) -> int {
    if b == 0 {
        panic("division by zero")
    }
    a / b
}

// Or with assertions
fn process(data: [int]) {
    assert(!data.is_empty(), "data cannot be empty")
    // ...
}

Panics are for bugs, not expected conditions. They crash the program (or thread in async context).

◉ Pipeline Error Handling

Multiple patterns for pipelines:

// Early return on first error
let result = items
    |> map(fn(i) { process(i)? })
    |> collect()?

// Collect all errors
let (successes, failures) = items
    |> map(process)
    |> partition_results()

// Filter out errors, keep successes
let results = items
    |> map(process)
    |> filter_ok()
    |> collect()

◉ Best Practices

Full error handling documentation in the language guide.