This chapter describes what happens when something goes wrong in a Mica program: the two kinds of failure and how each one travels, the error domains and the routines that fail with their codes, what a caller does with a failure, what reaches the caller and what ends the program, the traps and the tier that guards more of them, and what a failure does to a block of tasks, a stream and a worker process. Each construct names its rule in the grammar, says what it means, and quotes what the compiler answers when a source breaks the rule. The failure channel and error domains teach the same ground from the start, and traps, tiers, and what the compiler guards lists every trap.

Every example is a complete program the website’s gate compiles with the compiler as it is written here, and the output shown beneath it is the output the program prints.

Two kinds of failure

A program meets two kinds of failure, and each has a mechanism of its own:

An expected failureA bug
What it isthe world declined: a file is missing, a value lies outside a gauge’s scale, a descriptor is closedthe program is wrong: an index misses its array, a divisor is zero, a pointer is nil
How it is writtena routine states fails and an error domain, and fail raises one of its codesnothing: the compiler guards the act
What happensthe code travels to the caller, which must say what happens thenthe program stops at the act, with a report that names it
Who answers itevery caller, at the callnobody: no clause catches a trap

An expected failure is a value, and the language makes every caller decide what to do with it. A bug has no value to continue with, so a trap ends the program on every tier, and an on fail clause never runs for one: a call consumed with on fail use 1 that misses its array inside the routine stops with index_out_of_range all the same.

Error domains

An error domain is a type whose values are the codes a routine fails with, written error and its codes in parentheses (error-domain-type):

type
    GaugeError = error (Disconnected, OutOfRange);

A domain names one code at least. Its codes are ordinals numbered from zero in the order they are written, as an enumeration’s literals are, so Ord(OutOfRange) is 1, and a code is one value, carrying no text and no other data. A domain of a library or of a surface arrives by imp with its codes, each imported by name as any other member is: the POSIX surface’s OsError is the domain the C library’s error numbers arrive in, and its code Ebadf is imported as Close, OsError, Ebadf : posix;.

A case over a domain handles every code or carries an else, as a case over any ordinal does (chapter 6), and its refusal says so in the failure’s own words.

WrittenCodeThe compiler’s words
a domain without a code, E = error ();2008expected identifier, found right parenthesis
a case over E = error (A, B) whose one arm is A, with no else5300a case over the error domain 'E' must consume every code or carry an else: 1 of 2 codes are covered — an unhandled error code is the exact silent gap the failure channel exists to close

Routines that fail

A function, a procedure, a task and a generator may end their heading with fails and a domain (fails-clause): the routine may then fail with a code of that domain and of no other. A routine fails one domain, named in its heading, and a stream’s type states the domain its generator fails, stream of int64 fails GaugeError, as chapter 11 describes.

fail and a code ends the routine at once (fail-statement): its deferred statements run, as at any other end of the routine (chapter 6), the routine answers no result, and the code travels to the caller. fail stands only in a routine that states fails, and the code it names belongs to that routine’s domain.

WrittenCodeThe compiler’s words
a domain that is no domain, function G() : int64 fails int64;5301'fails' requires an error domain type, but 'int64' is declared as 'primitive': declare the domain as 'T = error (A, B, C);' and name it in the signature
fail A; in a function G without fails5302'fail' raises on the enclosing function's failure channel, so it requires a signature declaring 'fails': 'G' cannot fail
fail A; in a function that fails Other, A a code of E5303the raised code must be a code of the enclosing function's fails domain 'Other', found: E
a callback that states fails E5512a callback cannot declare 'fails': the foreign library that fires 'K' runs outside the failure channel and could neither read nor consume it — report failure through the callback's C-shaped return value

What a caller does with a failure

A call of a routine that fails is followed by a clause that says what happens when it does (value-consumption, statement-consumption). When the call succeeds, the clause does nothing, and when it fails, the clause decides:

WrittenStands behindWhen the call fails
level := Reading(raw) on fail use 0an assignmentthe value behind use is stored in the call’s place, of the type the assignment admits
level := Reading(raw) on fail failure do ..., Report(raw) on fail failure do ...an assignment or a call statementthe variable behind fail receives the code and the one statement behind do runs; the variable is declared beforehand, of exactly the callee’s domain
level := Reading(raw) on fail leave, Report(raw) on fail leavean assignment or a call statementthe routine the call stands in fails with the same code, its deferred statements running first; it fails the callee’s domain, and the program’s body never does
Report(raw) on fail continuea call statementthe failure is discarded and the next statement runs; a function called as a statement drops its answer, as it does without a failure

A routine that fails one domain and calls a routine that fails another passes no failure on with leave: its handler converts the code, raising one of its own domain with fail inside the statement behind do, as the second example shows. A for ... in loop over a stream that fails, and a select over such a stream, take the same clauses behind their end, as chapter 11 describes.

WrittenCodeThe compiler’s words
n := F(1);, F failing E5304a call to the fallible function 'F' must consume its failure channel: propagate with 'on fail leave', default with 'on fail use <expression>', handle with 'on fail e do <statement>', or discard a procedure's failure with 'on fail continue' — an unconsumed failure cannot exist
n := H() on fail use 0;, H failing nothing5305'H' cannot fail, so the call carries no failure channel to consume; remove the consumption clause
on fail leave in a procedure R without fails5306'on fail leave' forwards the failure on the enclosing function's own channel, so 'R' must itself declare 'fails'
on fail leave in the program’s body5306'on fail leave' forwards the failure on the enclosing function's own channel, so 'the program body' must itself declare 'fails'
on fail leave of a call failing E, in a function failing Other5307the failure cannot forward across domains: the callee fails 'E' but the enclosing function fails 'Other'; handle and convert explicitly instead
on fail o do, o of the domain Other, behind a call failing E5312the on-fail binding 'o' is declared as 'Other', but the callee fails 'E': declare the binding with exactly the callee's error domain so the caught code keeps its meaning
on fail z do, z declared nowhere5018identifier not found: z
n := F(1) on fail continue;2055'on fail continue' discards a procedure's failure and runs on, but a failed function call leaves no result to assign: default with 'on fail use <expression>' or handle with 'on fail e do <statement>'
Q(1) on fail use 0;, Q a procedure2075a call statement keeps no result a default could replace: discard the failure with 'on fail continue', handle it with 'on fail <name> do', forward it with 'on fail leave', or assign the call to take a default with 'on fail use'
n := F(1) on fail use "text";, n an int645143incompatible data types in assignment to n from 'on fail use': string to int64
defer Q(1) on fail leave;5314a deferred statement cannot propagate with 'on fail leave': cleanups run inside the activation's exit sequence, and a forward there would jump into the very sequence it is part of; handle with 'on fail e do' or discard with 'on fail continue' instead
{
    Failures - a gauge that can fail, and the four ways a caller answers its failure.

    'Reading' fails with 'Disconnected' below zero and with 'OutOfRange' above the scale, and every call of it says
    what happens then: 'on fail use' stores a default in the result's place, 'on fail failure do' runs a statement that
    reads the code, 'on fail leave' hands the failure on to a caller that fails the same domain, and 'on fail continue'
    discards a procedure's failure. 'Report' defers its closing line, which runs on the failure's road as on the
    success road.
}
program Failures;

imp
    WriteLn, Ord : std;

const
    Scale = 100;
    DefaultLevel = 50;
    NoLevel = -1;
    BelowZero = -5;
    AboveScale = 250;
    FirstRaw = 40;
    SecondRaw = 30;
    BrokenRaw = 999;
    InRange = 55;
    DefaultLine = "a reading below zero, defaulted: %lld";
    ClampedLine = "a reading above the scale, clamped: %lld";
    SumLine = "the sum of two readings: %lld";
    SumFailedLine = "the sum failed with code %lld";
    ReportLine = "  the gauge answered %lld";
    ClosedLine = "  the report is closed";

type
    { The ways a reading fails, each a code numbered from zero. }
    GaugeError = error (Disconnected, OutOfRange);

var
    level : int64;
    failure : GaugeError;

{ The level a raw value reads as, failing where the value lies outside the gauge's scale. }
function Reading(raw : int64) : int64 fails GaugeError;
begin
    if raw < 0 then
        fail Disconnected;
    if raw > Scale then
        fail OutOfRange;
    Reading := raw;
end;

{ The sum of two readings, handing the failure of either on to the caller. }
function SumOfReadings(first : int64, second : int64) : int64 fails GaugeError;
var
    left, right : int64;
begin
    left := Reading(first) on fail leave;
    right := Reading(second) on fail leave;
    SumOfReadings := left + right;
end;

{ A report of one reading, whose closing line runs whether the reading succeeds or fails. }
procedure Report(raw : int64) fails GaugeError;
var
    answer : int64;
begin
    defer WriteLn(ClosedLine);
    answer := Reading(raw) on fail leave;
    WriteLn(ReportLine, answer);
end;

begin
    level := Reading(BelowZero) on fail use DefaultLevel;
    WriteLn(DefaultLine, level);
    level := Reading(AboveScale) on fail failure do
        case failure of
            Disconnected: level := 0;
            OutOfRange: level := Scale;
        end;
    WriteLn(ClampedLine, level);
    level := SumOfReadings(FirstRaw, SecondRaw) on fail use NoLevel;
    WriteLn(SumLine, level);
    level := SumOfReadings(FirstRaw, BrokenRaw) on fail failure do
    begin
        WriteLn(SumFailedLine, Ord(failure));
        level := NoLevel;
    end;
    Report(InRange) on fail continue;
    Report(BelowZero) on fail continue;
end.

It prints:

a reading below zero, defaulted: 50
a reading above the scale, clamped: 100
the sum of two readings: 70
the sum failed with code 1
  the gauge answered 55
  the report is closed
  the report is closed

What reaches the caller, and the program

A failure travels one caller at a time, and only where a caller says so: each on fail leave hands the code to the next caller up, in the same domain, after the deferred statements of the routine it leaves have run, and every other clause ends the failure’s road where it stands. The program’s body states no fails, so every failure is consumed by the time it would reach the body, and a failure never ends a program.

A program ends with exit status 0 when its body ends, with the status it names when it calls the C library’s Exit (Exit(3) ends it with status 3), and with status 1 when a trap stops it.

A build at debug, and every checked build, records the road a failure takes: the fail that raised it and each on fail leave it passed, which FailureTracePrint writes to standard error from a handler and FailureTraceDepth counts, as the failure channel shows. A release build without checked records nothing.

A C program reads no failure channel: the header the compiler writes for a library leaves out every routine that fails and says why, as chapter 13 shows.

{
    Conversion - a failure of the operating system's domain, converted at the layer that receives it.

    'Close' of the POSIX surface fails with a code of 'OsError', the domain the C library's error numbers arrive in, and
    'Ebadf', one of its codes, is imported by name like any other member of the surface. 'Release' cannot hand that code
    on, since it fails a domain of its own, so its handler names the cause and raises 'Unavailable' in its place.
}
program Conversion;

imp
    WriteLn, Ord : std;
    Close, OsError, Ebadf : posix;

const
    MissingDescriptor = 99;
    BadDescriptorLine = "the operating system answered Ebadf, a descriptor no file is open on";
    OtherCauseLine = "the operating system answered code %lld";
    StoreLine = "the store answered code %lld, Unavailable";

type
    { The ways the program's store fails, whatever lies beneath it. }
    StoreError = error (Unavailable, Corrupt);

var
    failure : StoreError;

{ The release of a descriptor, the operating system's failure converted into the store's own domain. }
procedure Release(fileNumber : int32) fails StoreError;
var
    cause : OsError;
begin
    Close(fileNumber) on fail cause do
    begin
        case cause of
            Ebadf: WriteLn(BadDescriptorLine)
        else
            WriteLn(OtherCauseLine, Ord(cause));
        end;
        fail Unavailable;
    end;
end;

begin
    Release(MissingDescriptor) on fail failure do
        WriteLn(StoreLine, Ord(failure));
end.

It prints:

the operating system answered Ebadf, a descriptor no file is open on
the store answered code 0, Unavailable

Traps

A trap stops the program at the act that went wrong. It writes one report to standard error, naming the reason, the file, the line and the column, and the source line itself, and the program ends with exit status 1, on every tier and on both architectures. The report of the third example’s checked build reads:

Mica runtime failure: reason=signed_arithmetic_overflow (2)
Mica runtime context: file=Wrapping.mica, line=22, column=20
Mica runtime source:     small := small + Step;

The traps of every tier guard the acts that have no defined answer at all:

WrittenReason
n := 10 / d; with d zerodivision_by_zero
a[i] with i outside the array’s boundsindex_out_of_range
a value converted with as into a subrange or an enumeration that does not hold itordinal_out_of_domain
a matrix product of two dynamic matrices whose inner extents differtensor_shape_mismatch (chapter 12)
Backward called twice on one tape windowtape_misuse (chapter 12)
Panic("the gauge is gone");explicit_panic, with the text on a line of its own, Mica runtime message: the gauge is gone
recursion that exhausts the stackstack_overflow (chapter 7)

Traps, tiers, and what the compiler guards lists every one of them, with a nil pointer, an allocation that cannot be served and a file read past its end among them.

The checked tier

A build whose --optimize set names checked, debug,checked or O2,checked among them (the command line), guards more acts than the other tiers do. Without checked, an addition, subtraction or multiplication that leaves its integer type wraps, a narrowing cast keeps the low bits, a floating-point operation that produces a NaN or an infinity passes it on, and Assert is not compiled; with checked, each of those acts stops the program with a trap of its own, Assert(n = 3) that does not hold with assertion_failed, and a release of a cell twice, or a use of a cell after its release, with the trap that names it. A trap names the same act at the same place on every tier that guards it, and the checked tier’s table lists each act and the answer the other tiers give.

{
    Wrapping - an addition that leaves its type, answered by one tier and stopped by the checked one.

    127 is the largest value of an 'int8'. Adding one wraps to the smallest on a build without 'checked', and a checked
    build stops the program at the addition and names it.
}
program Wrapping;

imp
    WriteLn : std;

const
    Largest = 127;
    Step = 1;
    ResultLine = "127 + 1 in an int8 is %d";

var
    small : int8;

begin
    small := Largest;
    small := small + Step;
    WriteLn(ResultLine, small);
end.

Built at each tier and run, it prints:

--optimize debug
  127 + 1 in an int8 is -128
  exit=0
--optimize debug,checked
  Mica runtime failure: reason=signed_arithmetic_overflow (2)
  Mica runtime context: file=Wrapping.mica, line=22, column=20
  Mica runtime source:     small := small + Step;
  exit=1

Failure in tasks, streams and worker processes

A task that states fails hands its failure to the concurrent block that scheduled it, and the first failure in a block cancels the block’s other tasks and runs the block’s section for its domain, on fail failure do ..., after the join (handler-section). Every domain a block’s tasks fail is consumed there, by on fail continue, or by the routine around the block when it fails the same domain, as chapter 11 describes.

A generator that states fails ends its stream with a failure, which the loop or the select reading the stream consumes behind its end (chapter 11).

A worker process that ends before its work does, a trap in it among the causes, is lost rather than failed: the block that placed the work answers it in its on lost section (lost-section), and chapter 11 describes what a section then receives. A trap in a task on a carrier ends the whole program, as a trap in the program’s body does, which is a known limitation.

What a failure is not

  • A failure carries its code and nothing else. A program that wants to say more passes it through a result or a variable of its own.
  • A routine fails one domain. Crossing from one domain to another is a conversion the program writes in a handler, as the second example does.
  • An allocation that cannot be served is no failure of a domain: it is the trap allocation_failure, and the memory classes say where a fixed arena’s budget ends.
  • A callback fails nothing, because the C library that calls it reads no failure channel: it reports an outcome through its C-shaped result.