NURL

Error handling

Option, Result, the try operator, and panics.

NURL has no exceptions. A function that can fail says so in its return type. It uses one of two built-in types.

Option: ?T

?T represents a value that may be absent.

@ ?T { T payload }   // Some
@ ?T { F 0 }          // None (the second value is ignored on the None tag)

Match it with ??, using T / F as the tag pattern:

?? maybe_n {
    T n  ( nurl_print_int n )
    F ( nurl_print `nothing\n` )
}

Result: !T E

!T E carries either a success value of type T or an error value of type E.

@ !T E { T v }   // Ok
@ !T E { F e }   // Err
?? result {
    T v ( use v )
    F e  ( handle e )
}

The try operator: \

\ expr unwraps a ?T or !T E in place. On success it evaluates to the payload. On failure it returns early from the enclosing function and forwards the None / Err. The enclosing function's return type must be a compatible ? / ! shape.

@ parse_pair s src  ?i {
    : a \ ( parse_int src )     // returns None from parse_pair if this fails
    ^ @ ?i { T a }
}

This is the same idea as Rust's ? or Go's early-return-on-error pattern. NURL uses it as a prefix operator instead of postfix.

Leaf-site combinators

At a place that cannot use \ — a main that returns a fixed i, or a callback with a signature you cannot change — use a combinator. Do not nest ?? by hand. The common ones, for both ?T (stdlib/core/option.nu) and !T E (stdlib/core/result.nu):

OptionResultMeaning
opt_unwrap_or o defaultres_unwrap_or r defaultpayload, or a fallback value
opt_expect o msgres_expect r msgpayload, or panic with msg
opt_ok_or o errres_ok r / res_err rbridge between ?T and !T E
opt_map o fres_map r ftransform the success payload
opt_and_then o fres_and_then r fchain another fallible step
: i n ( opt_expect maybe_n `expected a parsed number` )

Prefer _expect with a message that names what you tried to do. It shows the message if the panic fires.

Panics

panic msg aborts the current logical operation. If nothing watches for it, the process prints msg to stderr and exits. recover wraps a closure call and turns a panic inside it into an Err, from stdlib/std/panic.nu:

: ~ HttpResponse resp ( response_text 500 `internal\n` )
: !v PanicInfo r ( recover \ v {
    = resp ( risky_handler req )
} )
?? r {
    T _  { /* resp holds risky_handler's return value */ }
    F p  { /* resp still holds the 500 default; p.msg has the reason */
            ( panic_info_free p ) }
}

Panics are a crash-mitigation tool, not a routine error path

Prefer !T E + \ for anything an expected, ordinary caller has to handle. Reach for panic / recover at a boundary like an HTTP request handler, where one request failing unexpectedly should not take down the whole process.

Recover is not exception handling in the C++ sense. There is no stack unwinding with destructor calls. A thread-local journal tracks every owned allocation the runtime made since the recover point. It reclaims the allocations if a panic fires. So ordinary owned strings, slices, and struct fields do not leak across a panic. It does not catch signal faults (SIGSEGV, SIGFPE, and similar signals still abort the process).

Next

Last updated on

On this page