← 13. Routines and calls · Contents · 15. Safety failures and traps →
14. Recoverable errors
14.1 Two failure classes
A recoverable error is an expected unsuccessful result that source code may propagate or handle. A trap is a non-recoverable safety failure defined by Chapter 15. Error handling does not intercept, convert, or resume after a trap.
Nucleus represents a recoverable error with a u8 code carried beside a routine's ordinary success result. The code has no separate error-set type. Programs give codes stable names with top-level u8 constants; Chapter 16 also defines the standard service codes. The value zero is permitted, although the standard codes are nonzero.
14.2 Failable signatures
A routine that can return a recoverable error writes fails at the end of its header:
routine-header ::= "sub" NAME "(" [ formal-parameter
{ "," formal-parameter } ] ")"
[ "as" type ] [ "fails" ]fails is part of the routine signature. A forward declaration records it once; the later abbreviated body header cannot repeat it. An ordinary routine without a forward includes it in its complete header. The clause does not change the declared parameters or optional success-result type.
Absent a trap, a failable invocation completes in exactly one of two ways:
- success, with the ordinary scalar value, aggregate alias, or no result declared by the header; or
- failure, with one
u8error code and no success result.
An infallible routine has only successful completion. It cannot use fail or propagate a callee's failure.
14.3 Producing failure
The statement
fail-statement ::= "fail" expressionends the current failable routine with failure. The expression is evaluated once and must be compatible with u8; an exact literal must fit, and u16 requires explicit checked narrowing. The activation ends after the code is obtained. No later statement in that routine executes.
fail in an infallible routine is invalid. A trap while evaluating the code remains a trap and does not become a recoverable error.
Named codes are ordinary constants:
const badDigit = 1
const tooLarge = 2
sub parseDigit(value as u8) as u8 fails
if value < '0' or value > '9'
fail badDigit
end
return value - '0'
end14.4 Required consumption
Every call of a failable routine must consume failure at that call site. Nucleus provides exactly two forms:
else failpropagates the code from the current failable routine.- Immediate
handle NAME ... endhandles the code locally.
A failable invocation cannot appear inside an argument, arithmetic operation, comparison, condition, index, general conversion, or other larger expression. It may be only:
- the complete initializer of a scalar local declaration, followed by
else fail; - the complete right side of an assignment, followed by
else failorhandle; - the complete routine-call statement, followed by
else failorhandle.
Local declarations admit propagation but not handling. return admits no failable invocation: it represents success only. An unconsumed failable invocation, two consumers on one invocation, or a failable invocation in any other position is invalid. Program-variable and constant initializers cannot call routines under Chapter 8 and therefore cannot be failable.
14.5 Propagation
The propagation suffix is:
failure-propagation ::= "else" "fail"On success, the surrounding declaration or assignment uses the callee's ordinary result, or the call statement continues. On failure, else fail immediately returns the same u8 code from the enclosing routine. The enclosing routine must declare fails.
sub loadByte() as u8 fails
var value as u8 = readStorageByte() else fail
return value
endPropagation is explicit at every intermediate call. Nucleus has no implicit propagation, error-set inclusion, code remapping, handler stack, or unwinding.
14.6 Local handling
handle NAME occurs on the same logical line as the assignment or routine-call statement whose direct failable invocation it handles:
failure-handler ::= "handle" NAME NEWLINE
statement-sequence "end" NEWLINEThe name must resolve to an existing writable u8 scalar variable, parameter, or local. A scalar local serving as an active counted-loop counter is read-only and cannot be the error destination. The clause declares no binding and opens no scope. This rule preserves the declaration-prefix and scope rules from Chapters 5 and 8.
On success, the call supplies its ordinary result, the assignment occurs when present, and the handler body is skipped. On failure, no success-result store occurs, then the compiler stores the error code in the named u8 destination and executes the handler body. This ordering also applies when the assignment destination and error destination are the same variable: the variable receives the error code. Normal completion of the body continues after its closing end. A return, fail, exit, or continue inside the body has its ordinary enclosing context.
sub copyOne()
var code as u8
var value as u8
value = readStorageByte() handle code
return
end
writeOutputByte(value) handle code
return
end
endThe handled call must be the complete right side of the assignment or the complete call statement. The handler begins after that line's NEWLINE; attachment state never survives the newline. A handler cannot attach to a local declaration, return, compound statement, infallible call, propagated call, or another statement.
14.7 Results, flow, and entry failure
Ordinary return denotes successful completion only. A result-free failable routine may use bare return or reach its closing end. A result-bearing failable routine must return a compatible success result or fail on every path under the fallthrough rules in Section 13.7, extended so fail does not fall through. A caller that needs to propagate a failable result does so in a preceding local initializer, assignment, or call statement, then returns only the successful result.
else fail can exit on failure and continue on success, so it does not by itself make following source unreachable. A handle body can complete normally unless it has a non-fallthrough statement on every path.
The fixed main routine may declare fails. A failure returned from main has no source caller and performs the unhandled-error trap in Chapter 15 with the returned code. A successful return from main terminates normally.
14.8 Lowering boundary
The source semantics require a success/failure discriminant and a u8 code for each failable result. The Z80 runtime and backend contract defines their required target behavior while leaving the carrier choice private. Carry plus a byte register is one possible calling convention, not source semantics.
Failure propagation is an ordinary conditional return. Local handling is an ordinary conditional branch. Nucleus has no exception object, stack walk, cleanup action, hidden handler registration, or resumable failure state. The all-caller-save-compatible call semantics in Chapter 13 apply to both outcomes.
14.9 Invalid forms and capacities
The compiler must diagnose:
failorelse failin an infallible routine;- a failure code incompatible with
u8; - a failable invocation in a nested expression or unsupported context;
- a failable invocation with no consumer or more than one consumer;
handleattached to an ineligible statement;- a propagating
returnform; - an error destination that is unavailable, non-writable, not
u8, or an active counted-loop counter; - a
failsclause or other signature text repeated on an abbreviated forward body; and - a result-bearing failable routine that can reach its end without success or failure.
An implementation may bound retained failable signatures, nested handlers, failure fixups, and active error destinations. It must publish each limit and issue a capacity diagnostic before exhaustion can discard a check, route a code to the wrong caller, or execute the wrong handler.