Skip to content

Error Handling

Masterplanner25 edited this page Jun 4, 2026 · 1 revision

Error Handling

Nodus provides structured error handling with try/catch/finally and throw. The exception model is stable as of v1.0.

Basic try/catch

try {
    let data = read_file("config.json")
    print(data)
} catch err {
    print("Failed: " + err.message)
}

try/catch/finally

fn read_config(path) {
    try {
        let data = read_file(path)
        return data
    } catch err {
        print("Error: " + err.message)
        return nil
    } finally {
        print("read_config done")
    }
}

The finally block always runs — on normal success, on caught exception, and on return from inside the try or catch block.

The Five Exit Paths

All five exit paths from a try/catch/finally are handled correctly:

Path Description finally runs?
1. Normal try body completes without error, no catch Yes
2. Caught exception exception in try, catch binds err and runs Yes
3. Uncaught exception exception in try, no matching handler, propagates Yes
4. return inside try return executes inside the try body Yes — before the function returns
5. return inside catch return executes inside the catch block Yes — before the function returns
fn example() {
    try {
        return "from try"
    } catch err {
        print("error")
    } finally {
        print("finally runs before the return completes")
    }
}
print(example())
// Output:
//   finally runs before the return completes
//   from try

The Error Object

Inside a catch block, err is a record with these fields:

Field Type Description
err.message string Human-readable error description (always present)
err.kind string Error category (see table below)
err.payload any Original thrown value when throw <non-string>; nil otherwise

Error kinds

Kind When raised
"name" Undefined variable accessed
"type" Type mismatch (e.g. add number to nil)
"index" List index out of range
"key" Map key not found
"thrown" throw <non-string value>
"sandbox" Runtime limit exceeded (steps, time, output, stack depth)
"import" Module resolution failure

throw: Strings vs Structured Values

Throw a string — becomes err.message:

throw "file not found"
// err.message = "file not found"
// err.kind    = "thrown"
// err.payload = nil

Throw a structured value — preserved as err.payload:

throw record { code: 404, reason: "not found" }
// err.kind    = "thrown"
// err.payload.code   = 404
// err.payload.reason = "not found"
// err.message = stringified representation
try {
    throw record { code: 404, reason: "not found" }
} catch err {
    print(err.kind)             // "thrown"
    print(err.payload.code)     // 404
    print(err.payload.reason)   // "not found"
}

Nested try Blocks

fn process(data) {
    try {
        try {
            let result = parse(data)
            return result
        } catch inner_err {
            print("parse error: " + inner_err.message)
            throw inner_err.message    // re-throw as string
        }
    } catch outer_err {
        print("outer caught: " + outer_err.message)
        return nil
    } finally {
        print("outer finally")
    }
}

Each try block has its own handler. An exception propagates up through nested handlers until caught.

finally Without catch

A finally clause requires a preceding catch clause. try { } finally { } without catch is a syntax error.

Runtime Error Types

Nodus raises two categories of errors from its own internals:

  • LangSyntaxError — raised by the lexer or parser (syntax, import resolution, cyclic imports)
  • LangRuntimeError — raised by the VM (name errors, type errors, index errors, etc.)

Both propagate into the catch block as the err record.

Stack Traces

Uncaught exceptions print a compact stack trace:

Type error at script.nd:15:9: Cannot add number and nil
Stack trace:
  at compute (script.nd:15:9)
  called from main (script.nd:22:5)

Common Patterns

Default value on error

fn safe_read(path) {
    try {
        return read_file(path)
    } catch err {
        return nil
    }
}

Error logging with re-throw

fn load(path) {
    try {
        return read_file(path)
    } catch err {
        print("load failed: " + err.message)
        throw err.message
    }
}

Resource cleanup with finally

fn with_resource(path) {
    let resource = nil
    try {
        resource = acquire(path)
        return process(resource)
    } catch err {
        print("error: " + err.message)
        return nil
    } finally {
        if (resource != nil) {
            release(resource)
        }
    }
}

See also Control Flow and Syntax Reference.

Clone this wiki locally