Error Handling

Errors in Risor are values you can inspect, and exceptions you can catch. Use try/catch/finally to intercept errors and throw to raise them -- and because try/catch is an expression, error recovery often fits in a single line.

Creating Errors

The error built-in creates an error value. It supports format string interpolation similar to Go's fmt.Sprintf:

let err = error("something went wrong")
let err = error("file %s not found", filename)

Error values are always truthy and can be inspected with the .message() method.

Throwing Errors

throw raises an error and interrupts normal execution:

throw error("something went wrong")

Execution unwinds up the call stack until a try/catch block catches it. If nothing catches it, the script terminates with the error.

Try / Catch / Finally

Wrap risky code in try/catch/finally to handle errors gracefully:

try {
    risky_operation()
} catch e {
    error("failed: %s", e.message())
} finally {
    cleanup()
}

Catch Block

catch runs when an error is thrown inside the try block. The variable after catch binds to the error value:

try {
    let data = parse(input)
} catch err {
    error("parse failed: %s", err.message())
}

Finally Block

finally runs regardless of whether an error occurred. Useful for cleanup:

let conn = open_connection()
try {
    conn.send(data)
} catch e {
    error("send failed: %s", e.message())
} finally {
    conn.close()
}

finally is optional. Use try/catch without it, or try/finally without catch. Both work.

Try as an Expression

Like if/else and match, try/catch is an expression. It evaluates to the last expression in whichever branch runs:

let result = try {
    parse(input)
} catch e {
    default_value
}

This makes fallback values a one-liner:

let config = try { load_config() } catch e { {} }

Error Methods

Error values support the following method:

MethodDescription
.message()Returns the error message as a string

Example:

let err = error("disk full")
err.message()   // "disk full"

Patterns

Wrapping Errors

Add context when re-throwing:

try {
    connect(host)
} catch e {
    throw error("connection to %s failed: %s", host, e.message())
}

Safe Value Extraction

try/catch as an expression gives you safe extraction with a fallback:

let port = try { int(config["port"]) } catch e { 8080 }

Validation

Fail fast by validating preconditions up front:

function divide(a, b) {
    if (b == 0) {
        throw error("division by zero")
    }
    return a / b
}

Risor © 2026 Deep Noodle