The design starts from a conclusion the field converged on the hard way, from CLU through Midori to Rust, Swift, and Zig: bugs and expected errors must not share a mechanism.

A file that does not exist is a situation your program is supposed to handle. An index that is out of range is a defect in your program. Routing both through one construct means either you handle defects as though they were routine, or you ignore genuine errors as though they were impossible.

Two kingdoms

Expected errors travel on a channel declared in the signature:

type
    ParseError = error (BadDigit, Overflow, EmptyInput);

function ParseDigit(c : int32) : int64 fails ParseError;
begin
    if c < 0 then
        fail EmptyInput;
    ParseDigit := c as int64;
end;

Three consumption forms cover the cases, and each is explicit at the call site:

value := ParseDigit(c) on fail leave;          { propagate to my caller }
value := ParseDigit(c) on fail use -1;         { substitute a default }
value := ParseDigit(c) on fail e do            { inspect and branch }
             Report(e)
         else
             value := 0;

Bugs do not use this path at all. Twenty runtime failure reasons — bounds violations, checked-arithmetic overflow, failed casts — terminate the process deterministically in every optimization tier. That is the corruption floor: a defective program stops, loudly and at its source location, rather than continuing into undefined behaviour.

Out of band, by design

The channel carries no payload types. There are no sum types, no variant records, no payload enums, and no tuples in the language, so error handling does not drag a type-level construction into every signature that might fail. One error domain per function; converting between domains is an explicit re-raise.

Error return traces are available on debug and checked builds. Release pays zero — the channel compiles away to the control flow it describes.

The C boundary comes for free

An imported C function is a failing function. Its convention — negative errno, NULL return, or a sentinel value — is declared once in the interop contract and lifted at the call site into this same channel:

sock := Socket(AF_INET, SOCK_STREAM, 0) on fail use -1;

No -1 comparison, no NULL check, no errno read in user code. See the C boundary.

See also