Error Handling
Nilakan represents selected recoverable failures as explicit err values. Only operations whose
contracts register a recoverable failure may produce one. Syntax, type, arity, raw access,
destructuring, mutation, resource, hashability, invariant, and other fatal diagnostics are never
implicitly converted into values.
Creating And Returning Errors
Section titled “Creating And Returning Errors”The err type is a scalar value containing a name, message, diagnostic code, and runtime creation
origin. Nilakan source constructs one with err(name, message, code):
failure: err = err("ValidationError", "missing account", "APP0001")print(failure)A binding or function contract must explicitly accept a direct error. A root err or a root union
containing err accepts one:
failure: err = err("ValidationError", "missing account", "APP0001")outcome: num | err = failure
def identity(value: num | err) -> num | err: return valueThe root policy is shallow. A nested err permits errors only at that nested position:
items: list[num | err] = [1, err("Missing", "no value", "APP0002")]list[num] rejects an error in an element, while list[num] | err accepts either an entirely
successful number list or a direct error. An unannotated binding and a root contract without err
reject results that are known to be, or may be, direct errors. Recover or declare the union before
assigning, returning, destructuring, or discarding such a result.
Annotations persist across later assignment, destructuring, subscript assignment, primed updates, and commit-loop state. The enclosing contract always wins over a storage operation’s generic ability to retain values:
values: list[num] = [1]values = values.set(0, err("Failure", "bad value", "APP0003")) # NLK2011Sets and dictionary keys are always deeply hashable and error-free. Ordinary container values can store errors when their enclosing contract permits them.
Safe Access And Registered Failures
Section titled “Safe Access And Registered Failures”Raw indexing remains fatal when an index or key is missing. Use get when absence is an expected
value-level outcome:
first: num | err = [10, 20].get(0)missing: num | err = [10, 20].get(10)letter: str | err = "Nilakan".get(1)entry: num | err = {"ready": 1}.get("missing")fallback: num = {"ready": 1}.get("missing", 0)str.get, list.get, and tuple.get return IndexError/NLK4004 on a miss. One-argument
dict.get returns KeyError/NLK4003; two-argument dict.get returns its default without a
missing-key error. A successfully extracted stored error keeps its identity and creation origin.
Other registered value failures include the implemented ord, chr, string search and validation,
list and tuple search, list and set removal/pop, and dictionary pop/removal operations. Each
operation contract defines its own recoverable tags. Nilakan does not use a blanket
runtime-exception conversion rule.
catch: Local Recovery
Section titled “catch: Local Recovery”left catch fallback handles only a direct error result from left. Evaluation is lazy:
- a successful left value is returned and the fallback is not evaluated;
- an error-only left result evaluates only the fallback;
- a left result that may be success or error preserves the success path and evaluates the fallback for the error path.
value = [10, 20].get(8) catch 0catch consumes the left error. An error returned normally by the fallback remains an ordinary
result error. Propagation paths bypass an outer catch; they are not converted back into the left
expression’s direct-error branch. A catch whose left side has no normal direct-error branch is
NLK2010 even when that side can propagate, so (try operation()) catch fallback is rejected rather
than acting like exception recovery.
Using catch on a statically success-only expression is NLK2010. A statically unreachable left
expression remains unreachable without evaluating the fallback or producing that diagnostic. catch
is right-associative, so a catch b catch c parses as a catch (b catch c).
try: Function-Scoped Propagation
Section titled “try: Function-Scoped Propagation”try operand handles only a direct error in the operand’s normal result:
- success continues unchanged;
- a possible error is removed from the normal value and added to the current function’s propagated result;
- an error-only result stops that path and propagates the error.
Existing propagation is preserved and bypasses an outer try. Applying try where no direct error
can be handled is NLK2010. Top-level try is also NLK2010. Inside a function, try is allowed only
when the root return contract accepts a direct error.
Propagation belongs to one function invocation. At its boundary, accepted propagated errors are
reified as direct return results. Use try when successful execution has more work to do and an
error should skip that work:
def first_plus_one(values: list[num]) -> num | err: first = try values.get(0) return first + 1
def first_plus_two(values: list[num]) -> num | err: first = try first_plus_one(values) return first + 1
failure: num | err = first_plus_two([])This program has two separate propagation events. The error returned from first_plus_one is a
normal result at the caller boundary, and the caller’s try propagates it again. In contrast,
return values.get(0) needs no try: directly returning an accepted error result has no later work
to skip.
Arguments are evaluated in the caller. Omitted defaults are evaluated sequentially in the callee
after its frame is created, so a try in a default exits the function that owns the default.
Supplying that parameter skips both the default evaluation and its effect. Parameter and default
values follow the same annotation-derived error policy.
Matching And Narrowing
Section titled “Matching And Narrowing”is_err accepts optional keyword-only, exact, case-sensitive filters:
is_err(value)is_err(value, name="IndexError")is_err(value, code="NLK4004")is_err(value, name="IndexError", code="NLK4004")When both filters are present they are combined with AND. A matching branch narrows a stable bare
variable to the selected error tags; the false branch excludes known matching tags. not reverses
the facts, and each elif receives the accumulated false facts. Writing the variable invalidates
its prior narrowing.
err_name, err_message, and err_code require a value known to be a direct error. Narrow a
success-or-error union with is_err before inspecting it. The inspectors’ string results can be
compared normally.
Comparison Safety
Section titled “Comparison Safety”Errors cannot participate in equality, inequality, membership comparison, collection search,
count, index, or remove. This also applies when structural traversal reaches an error inside a
container. A known violation is NLK2013; an unknown shape is checked at runtime and rejected as
NLK4006.
Errors are never compared by name, message, code, origin, or container position. Use the inspectors
or filtered is_err when matching is intentional.
Evaluation And Commit Flow
Section titled “Evaluation And Commit Flow”Eager expression children are evaluated left to right. Once a child propagates, later eager children
do not transfer runtime effects. and, or, conditional expressions, and catch fallbacks
evaluate only the branch required by their left operand or condition. Statically impossible branches
are excluded.
In a commit loop, propagation abandons the round’s staged next-state writes. Flow analysis includes the zero-iteration path and joins continuing branch state while keeping propagated errors separate.
Diagnostics
Section titled “Diagnostics”The public diagnostic name for the static rules remains StaticError.
| Code | Meaning |
| --------- | -------------------------------------------------------------- |
| NLK2009 | Direct error reached a generic rejecting or discarded position |
| NLK2010 | Invalid try or catch |
| NLK2011 | Root or nested type-contract mismatch |
| NLK2013 | Direct or contained error comparison |
| NLK2014 | Operation requirement violation, including error inspection |
| NLK4006 | Runtime rejection of an error in an unknown shape |
| NLK5999 | Interpreter invariant failure |
Known recursive type and operation violations are reported statically. Unknown shapes defer to the authoritative recursive runtime checks. NLK2011 identifies the failing structural path and the expected and inferred types when known. Diagnostics for raw access, rejected direct errors, comparison, and error inspection include recovery guidance.
Provenance And Completion
Section titled “Provenance And Completion”Every error value carries an enumerable structured creation origin. Copying, deep copying, structured cloning, worker transfer, safe extraction, CLI execution, and playground execution preserve it. Runtime NLK4006 diagnostics keep creation and rejection stacks separately, innermost first, retaining up to 16 frames and rendering up to five from each stack. Other fatal runtime diagnostics render an innermost-first call stack when execution crossed a function boundary.
Printing, retaining, or validly returning an error is successful execution. The CLI exits with
status 0, and the playground reports Completed. Only an invalid use or fatal diagnostic fails the
run.