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 failure | A bug | |
|---|---|---|
| What it is | the world declined: a file is missing, a value lies outside a gauge’s scale, a descriptor is closed | the program is wrong: an index misses its array, a divisor is zero, a pointer is nil |
| How it is written | a routine states fails and an error domain, and fail raises one of its codes | nothing: the compiler guards the act |
| What happens | the code travels to the caller, which must say what happens then | the program stops at the act, with a report that names it |
| Who answers it | every caller, at the call | nobody: 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.
| Written | Code | The compiler’s words |
|---|---|---|
a domain without a code, E = error (); | 2008 | expected identifier, found right parenthesis |
a case over E = error (A, B) whose one arm is A, with no else | 5300 | a 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.
| Written | Code | The 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 fails | 5302 | '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 E | 5303 | the raised code must be a code of the enclosing function's fails domain 'Other', found: E |
a callback that states fails E | 5512 | a 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:
| Written | Stands behind | When the call fails |
|---|---|---|
level := Reading(raw) on fail use 0 | an assignment | the 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 statement | the 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 leave | an assignment or a call statement | the 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 continue | a call statement | the 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.
| Written | Code | The compiler’s words |
|---|---|---|
n := F(1);, F failing E | 5304 | a 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 nothing | 5305 | 'H' cannot fail, so the call carries no failure channel to consume; remove the consumption clause |
on fail leave in a procedure R without fails | 5306 | '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 body | 5306 | '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 Other | 5307 | the 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 E | 5312 | the 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 nowhere | 5018 | identifier 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 procedure | 2075 | a 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 int64 | 5143 | incompatible data types in assignment to n from 'on fail use': string to int64 |
defer Q(1) on fail leave; | 5314 | a 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 closedWhat 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, UnavailableTraps
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:
| Written | Reason |
|---|---|
n := 10 / d; with d zero | division_by_zero |
a[i] with i outside the array’s bounds | index_out_of_range |
a value converted with as into a subrange or an enumeration that does not hold it | ordinal_out_of_domain |
| a matrix product of two dynamic matrices whose inner extents differ | tensor_shape_mismatch (chapter 12) |
Backward called twice on one tape window | tape_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 stack | stack_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=1Failure 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.