Errors

Stone does not have null or undefined. All values are always well-defined. Absence, failure, or emptiness must be represented explicitly, never implicitly.

Error Model

Stone uses runtime traps for error conditions. When an error occurs, execution stops immediately with a clear error message. There are no sentinel values for failure.

Error Conditions

The following conditions produce runtime errors:

Index out of bounds: Accessing an array element with an index outside 0..len(array)-1.

xs = [1, 2, 3]
x = xs[5]  // ERROR: index 5 out of bounds for array of length 3

Missing object field: Accessing a field that doesn't exist on an object.

point = { x = 10, y = 20}
z = point.z  // ERROR: field 'z' does not exist

Missing extracted field: Extracting a field that doesn't exist.

point = { x = 10, y = 20}
x, z := point  // ERROR: no field 'z' in object

Array length mismatch: Using the same index variable with arrays of different lengths in mapping.

xs = [1, 2, 3]
ys = [10, 20]
sums[i] = xs[i] + ys[i]  // ERROR: length mismatch (3 vs 2)

Division by zero: Dividing by zero.

x = 10 / 0  // ERROR: division by zero

Empty array pop: Calling pop() on an empty array.

xs = []
result = pop(xs)  // ERROR: cannot pop from empty array

Invalid slice indices: Calling slice() with out-of-bounds or invalid indices.

xs = [1, 2, 3]
subset = slice(xs, -1, 5)  // ERROR: invalid indices

Invalid terminal method: Calling a method that doesn't exist on a terminal.

import graph2d from plot
g = graph2d({})
g.invalidMethod("test")  // ERROR: method 'invalidMethod' does not exist

Invalid next-state reference: Referencing v' before it has been assigned in the current iteration.

evolve (x = 0) for (i in items) {
    y = x' + 1  // ERROR: x' not yet assigned
    x' = i
}

Multiple next-state assignments: Assigning to v' more than once in the same iteration.

evolve (x = 0) for (i in items) {
    x' = i
    x' = i + 1  // ERROR: x' already assigned in this iteration
}

Error Recovery with try/else

The try/else expression catches runtime errors and provides a fallback value instead of trapping:

value = try parse_number(input) else { 0 }

If the expression succeeds, its result is used. If it throws, the else block runs and its last expression becomes the value.

To access the error message, bind it with a parameter:

value = try parse_number(input) else (err) {
    print("Failed: " + err)
    0
}

Block form for multi-statement try bodies:

config = try {
    content = file.read("config.json")
    parse(content, Config)
} else {
    print("Using default config")
    { debug = false, max_items = 100 }
}

try/else is an expression — it always produces a value and both branches must return the same type when used in an assignment.

Throwing Custom Errors

Use the error() built-in function to throw custom runtime errors:

fn cross(a, b) {
    if (len(a) != 3 || len(b) != 3) {
        error("cross() requires 3D vectors")
    }
    [
        a[1] * b[2] - a[2] * b[1],
        a[2] * b[0] - a[0] * b[2],
        a[0] * b[1] - a[1] * b[0]
    ]
}

fn safe_sqrt(x) {
    if (x < 0) {
        error("Cannot take square root of negative number")
    }
    sqrt(x)
}

Errors thrown with error() propagate up the call stack just like other runtime errors. The error message appears in the console output.

Type Errors

Type errors are caught at compile time and block compilation — no binary is produced when type errors exist.

Missing fields:

fn process(obj: {x: num, y: num}) = obj.x + obj.y
process({x = 1})  // ERROR: Missing field: y

Non-existent field access:

point = {x = 1, y = 2}
z = point.z  // ERROR: Field 'z' does not exist on record

Bracket access on records:

p = {x = 1}
v = p["x"]  // ERROR: Bracket indexing is supported on arrays only.
            // Use dot access (obj.field) or get(obj, key).

Overload mismatch:

import dot from linalg

M = [[1, 2], [3, 4]]
dot(M, M)  // ERROR: No matching overload for dot(array<num,2>, array<num,2>)
           // Available: (array<num,1>, array<num,1>) -> num

Width subtyping: Extra fields are allowed — {x=1, y=2} satisfies {x: num}.

Explicit Absence Patterns

When you need to represent optional or potentially-missing values, use explicit patterns:

Safe object access: Use get(obj, key) for non-trapping key lookup.

user = { name = "Alice"}

// Direct access traps on missing keys
// email = user.email  // ERROR: field 'email' does not exist

// get returns { ok, value } instead of trapping
result = get(user, "email")
if (result.ok) {
    print(result.value)
} else {
    print("No email found")
}

Result objects: Return {ok = false, value = 0} / {ok = true, value = result} to indicate success/failure.

fn safe_divide(a, b) = if (b == 0) {
    { ok = false, value = 0 }
} else {
    { ok = true, value = a / b }
}

result = safe_divide(10, 0)
if (result.ok) {
    print(result.value)
} else {
    print("Cannot divide by zero")
}

Found flags: Use boolean flags with loops.

found, value := evolve { found = false, value = 0 } for (x in xs) {
    if (x == target) {
        found' = true
        value' = x
        break
    }
}