Lambda Error Handling

March 20, 2026 · View on GitHub

This document covers Lambda's error handling system — how errors are created, returned, propagated, and enforced at compile time.

Related Documentation:


Table of Contents

  1. Overview
  2. The error Type
  3. Creating Errors
  4. The raise Keyword
  5. Error Return Types (T^E)
  6. Error Handling at Call Sites
  7. Error Propagation (^ Operator)
  8. Error Destructuring (let a^err = expr)
  9. Compile-Time Enforcement
  10. System Functions That Can Raise
  11. Error Code Categories
  12. Checking for Errors
  13. Error Truthiness and Defaults
  14. Examples

Overview

Lambda adopts an error-as-return-value paradigm. There is no try/throw/catch exception handling. Instead:

  • Errors are explicit values that flow through the type system.
  • Functions declare whether they can fail using the T^E return type syntax.
  • The raise keyword is the only way to return an error from a function.
  • The compiler enforces that callers handle errors — ignoring an error is a compile-time error.

This approach is inspired by Rust's Result<T, E> and Zig's T!E, but with lighter syntax.


The error Type

Lambda has a built-in error type with the following fields:

FieldTypeDescription
codeintError code (see Error Code Categories)
messagestringHuman-readable error message
filestring?Source file where the error occurred
lineint1-based line number
columnint1-based column number
sourceerror?Wrapped/chained inner error

Accessing Error Fields

let result^err = divide(10, 0)
if (^err) {
    print("code: " ++ str(err.code))
    print("message: " ++ err.message)
    if (err.source is error)
        print("caused by: " ++ err.source.message)
}

Creating Errors

The error() function constructs error values:

// Simple error with message (code defaults to 318 = user_error)
error("something went wrong")

// Error wrapping another error (for error chaining)
error("failed to load config", inner_err)

// Error with parameter map (full control)
error({
    code: 304,
    message: "division by zero"
})

Constructor signatures:

SignatureDescription
error(message: string)Creates user error with message
error(message: string, source: error)Creates error wrapping another error
error(params: map)Creates error with explicit fields

The raise Keyword

raise is the only way to return an error from a function. It is distinct from return, which only returns normal values.

fn divide(a, b) int^ {
    if (b == 0) raise error("division by zero")
    else a / b
}

Key properties:

  • raise immediately returns the error value to the caller.
  • A function must declare an error return type (T^ or T^E) to use raise.
  • Using raise in a function with a plain T return type is a compile error.
// ❌ Compile error: function does not declare error return
fn pure_add(a, b) int {
    raise error("oops")
}

Error Return Types (T^E)

Functions declare whether they can fail using the ^ suffix on their return type:

SyntaxMeaning
TAlways succeeds — no error possible
T^May return T or any error (shorthand for T^error)
T^EMay return T or a specific error type E
T^E1 | E2May return T or one of multiple error types
// Always succeeds
fn add(a: int, b: int) int => a + b

// May fail with any error
fn parse(s: string) int^ { ... }

// May fail with a specific error type
fn divide(a: int, b: int) int^DivisionError { ... }

// May fail with multiple error types
fn load(path: string) Config ^ ParseError | IOError { ... }

Note: The error type after ^ is restricted to error, identifiers, or unions of these. Complex type expressions (maps, arrays) are not allowed — define a named type instead.

Error Union in Parameters and Let Bindings

The T^ syntax also works in parameter types and let bindings:

// Parameter that accepts a value-or-error
fn process(input: int^) int { ... }

// Let binding that may hold a value-or-error
let result: int^ = may_fail(x)

Error Handling at Call Sites

When calling a function that returns T^, the caller must handle the error in one of the allowed ways:

PatternMeaningAllowed?
let a = F()^Propagate error, bind unwrapped value
let a^err = F()Capture value and error explicitly
F()^Propagate error, discard value
let a = F()Ignoring error❌ Compile error
F()Ignoring error and value❌ Compile error

Error Propagation (^ Operator)

The ^ postfix operator on a call expression unwraps the success value or propagates the error to the caller:

fn compute(x: int) int^ {
    let a = parse(input)^      // if error, return it immediately
    let b = divide(a, x)^      // if error, return it immediately
    a + b                       // normal return
}

Semantics:

  • If the call returns an error, ^ immediately returns that error from the enclosing function.
  • If the call succeeds, the expression evaluates to the unwrapped success value.

Rules:

  • ^ is only valid on calls to functions that can raise errors.
  • Using ^ on a non-error-returning function is a compile error:
    let x = add(1, 2)^  // ❌ Compile error: 'add' does not return errors
    
  • The enclosing function must itself declare an error return type to use ^.

Error Destructuring (let a^err = expr)

The ^ in a let binding captures both the success value and the error into separate variables:

let result^err = divide(10, x)
if (^err) {
    print("error: " ++ err.message)
    0  // default value
} else {
    result * 2
}

Semantics:

  • If the expression returns an error: the value variable (result) is null, the error variable (err) holds the error.
  • If the expression succeeds: the value variable holds the value, the error variable is null.
  • Use ^err (prefix ^) to test whether the error variable is an error (see Checking for Errors).

This is the primary way to handle an error locally rather than propagating it.


Compile-Time Enforcement

Lambda refuses to compile code that ignores errors. This is a language rule, not a warning.

Why?

Go's lack of enforcement is widely criticized — ignored errors cause production bugs. Rust's #[must_use] is a warning that can be suppressed. Lambda and Zig take the strongest stance: the compiler enforces it.

Rules

  1. Error-returning function calls must be handled

    divide(10, 0)           // ❌ compile error: unhandled error return
    let x = divide(10, 0)   // ❌ compile error: unhandled error return
    
  2. Functions with plain T return type cannot contain raise

    fn pure_add(a, b) int {
        raise error("oops")  // ❌ compile error
    }
    
  3. ^ is only valid on error-returning calls

    let x = add(1, 2)^  // ❌ compile error: 'add' does not return errors
    

Comparison Across Languages

AspectGoRustZigLambda
Can ignore error?✅ Yes⚠️ Warning (#[must_use])❌ Compile error❌ Compile error
Propagation syntaxManual if err != nilf()?try f()f()^
Destructure errorval, err := f()match on Resultcatch |err|let val^err = f()
Error type(T, error) tupleResult<T, E> enumT!E error unionT^E
Type preserved?⚠️ Interface (erased)✅ Static✅ Static✅ Static
EnforcementConvention onlyLint attributeLanguage ruleLanguage rule

System Functions That Can Raise

The following built-in functions perform I/O and may fail. They enforce the same error handling rules as user-defined T^ functions:

Pure I/O Functions

FunctionDescription
input(source)Read and parse data from file or URL
input(source, format)Read with explicit format

Procedural I/O Functions

FunctionDescription
output(data, target)Write data to file
output(data, target, options)Write with format/options
cmd(command)Execute shell command
io.copy(src, dst)Copy file or directory
io.move(src, dst)Move/rename file or directory
io.delete(path)Delete file or directory
io.mkdir(path)Create directory (recursive)
io.touch(path)Create file or update timestamp
io.symlink(target, link)Create symbolic link
io.chmod(path, mode)Change file permissions
io.rename(old, new)Rename file or directory
io.fetch(url)Fetch content from URL
io.fetch(url, options)Fetch with options

Example: Handling System Function Errors

// ❌ Compile error: unhandled error from 'input'
let data = input("file.json")^

// ✅ Propagate error
let data = input("file.json")^

// ✅ Capture error explicitly
let data^err = input("file.json")

// ❌ Compile error: unhandled error from 'io.mkdir'
io.mkdir("output")

// ✅ Propagate error
io.mkdir("output")^

Error Code Categories

Errors are categorized by numeric code ranges:

RangeCategoryDescription
1xxSyntax ErrorsLexical and grammatical errors during parsing
2xxSemantic ErrorsType checking, compilation errors
3xxRuntime ErrorsExecution-time failures
4xxI/O ErrorsFile, network, external resource errors
5xxInternal ErrorsUnexpected internal states

Common Error Codes

CodeNameDescription
201type_mismatchType incompatibility
202undefined_variableReference to undefined variable
203undefined_functionReference to undefined function
206argument_count_mismatchWrong number of arguments
228unhandled_errorError from can-raise function not handled
301null_referenceNull dereference
302index_out_of_boundsArray index out of range
304division_by_zeroDivision or modulo by zero
318user_errorUser-defined error via error()
401file_not_foundFile does not exist
402file_access_deniedPermission denied
407parse_errorError parsing input format

User-created errors (via error("message")) default to code 318 (user_error).


Checking for Errors

There are two ways to test whether a value is an error:

^expr — Error Check Shorthand

The ^ prefix operator is the idiomatic way to check for errors. It returns true if the operand is an error, false otherwise:

let result^err = divide(10, x)
if (^err) {
    print("Error: " ++ err.message)
}

This is especially concise with error destructuring:

let data^err = input("file.json")
if (^err) { raise error("failed to load", err) }

expr is error — Type Check

The is operator also works for error testing, and reads more naturally in some contexts:

let result = some_operation()
if (result is error) {
    print("Error occurred: " ++ result.message)
}

^expr is equivalent to expr is error — use whichever reads better in context.


Error Truthiness and Defaults

Error values are falsy in Lambda. This means:

  • if (err) treats errors the same as null or false — the condition is not entered.
  • Use ^err (not bare err) to check for errors in conditions.

Because errors are falsy, the or operator provides a natural default value pattern:

// If divide returns an error, fall through to 0
let safe_result = divide(10, x) or 0

// Chain with error destructuring for logging + default
let result^err = parse(input)
if (^err) { print("Warning: " ++ err.message) }
let value = result or default_value

Why Errors Are Falsy

PatternWith falsy errorsWith truthy errors
err or default✅ Falls through to default❌ Returns the error
if (^err)✅ Explicit error check
if (err)Treats error like nullWould enter the branch

Falsy errors enable the or default idiom and prevent accidental use of error values as truthy conditions. The ^ prefix provides an explicit, lightweight error check.


Examples

Basic Error Handling

fn may_fail(x) int^ {
    if (x < 0) raise error("negative input")
    else x * 2
}

// Propagate with ^
may_fail(5)^

// Destructure to handle locally
let result^err = may_fail(-1)
if (^err) {
    print("Got error: " ++ err.message)  // "negative input"
}

Chaining Calls with ^

fn compute(x) int^ {
    let doubled = may_fail(x)^    // propagate if error
    doubled + 10                   // normal return
}

compute(5)^   // 20

File Processing

fn process_file(path: string) ProcessedData^ {
    let content = input(path, 'text')^
    let lines = split(content, "\n")
    let parsed = (for line in lines parse_line(line)^)
    aggregate(parsed)
}

Error Wrapping

fn load_config(path: string) Config^ {
    let content^file_err = input(path, 'text')
    if (^file_err)
        raise error("failed to read config file", file_err)

    let parsed^parse_err = input(content, 'json')
    if (^parse_err)
        raise error("invalid JSON in config", parse_err)

    parsed
}

Procedural Script with Error Handling

pn main() {
    let config = input("config.json", 'json')^

    for item in config.items {
        let result^err = process(item)
        if (^err) {
            print("Warning: " ++ err.message)
            continue
        }
        output(result, "output/" ++ item.name ++ ".json")^
    }

    print("Done")
}

Summary

ConceptSyntaxPurpose
Declare error returnfn F() T^ or fn F() T^EFunction may fail
Create errorerror("message")Construct error value
Raise errorraise error("...")Return error from function
Propagate errorF()^Unwrap or auto-return error
Capture errorlet val^err = F()Destructure into value + error
Check for error^err or x is errorTest if value is an error
Default on errorexpr or defaultErrors are falsy, fall through to default

Lambda's error handling provides Go's simplicity (errors as values), Rust's safety (^ propagation, type enforcement), and Zig's strictness (compiler-enforced handling) — with concise, readable syntax.