This is part 6 of the series. Everything so far computed values that were simply there. This part opens a file — and a file is the first thing in the series that is allowed to not be there. What happens next is the part of a language most programs meet on their worst day, so Mica makes it part of the signature, part of the call, and part of what the compiler checks — not a convention you remember under pressure.
The example is examples/FirstFailure
in the tutorial repository: it counts the lines of a file named on the
command line, notes.txt by default.
Getting the file
make -C examples/FirstFailure runnotes.txt has 3 linesNow the road this part exists for — ask for a file that is not there. The built program is beside the example; run it with an argument:
$ examples/build/FirstFailure/FirstFailure no-such-file.txt
there is no file called no-such-file.txt hereNo stack dump, no numeric mystery — a sentence, because the program was made to say one. Here is how.
The example, walked
The signature admits it
function CountLines(path : string) : int64 fails OsError;fails OsError is the honest half of the return type: this function answers
a count or a reason it could not. Counting lines needs the operating
system, and the operating system can say no — the signature says so where
every caller reads it. A function with no fails cannot fail; you have been
relying on that in every part so far without knowing it.
Propagation is a word, not a pyramid
f := Open(path, ModeRead) on fail leave;
defer Close(address f);
got := ReadLine(f, address buf) on fail leave;Inside CountLines, every fallible call carries on fail leave: if the
callee fails, forward that failure on my own channel, now. The honest
signature costs one clause per call. And defer Close — registered beside
the Open it cleans up after — runs on the failure road too, because a
forwarded failure is an ordinary exit. Nothing on any road leaks the file.
The call you cannot leave unfinished
lines := CountLines(path) on fail caught do
begin
case caught of
Enoent :
WriteLn("there is no file called %ls here", path)
else
WriteLn("%ls could not be read, code %lld", path, Ord(caught))
end;
leave;
end;The caller consumes the failure with on fail caught do — caught is the
reason as an ordinary value, and a case reads it like any other. The
missing file has a name, Enoent, and the else keeps every other reason
honest. leave ends the program’s block: there is no line count to print on
this road, so the road ends.
What you cannot do is write lines := CountLines(path); and move on — the
second experiment shows what the compiler says about that.
Try it
1. The other reasons exist too. Ask for a file you may not read:
$ examples/build/FirstFailure/FirstFailure /etc/shadow
/etc/shadow could not be read, code 10The else arm earned its keep: not a missing file, so not Enoent — a
permission wall, printed by its code here. The
error domains tutorial is where the whole family
gets its names.
2. Try to ignore the failure. Delete the entire on fail caught do begin … end clause, leaving lines := CountLines(path);, and rebuild:
analyzer error 5304: a call to the fallible function 'CountLines' 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 existThat message is the whole design in one sentence: four ways to consume a failure, and no way to not choose. The forgotten error path — the one that works in every demo and detonates in production — does not compile here.
3. Break the cleanup on purpose — and fail. Delete defer Close(address f);
and run the missing-file road again. The behavior does not change — the file
never opened, so there was nothing to close — and the present-file road still
works, with the descriptor now returned only when the program exits. The
point of defer is not this small program; it is that the cleanup was
written once, beside the resource, and rode every road out. Put it back.
What the compiler proved
A function that can fail says so in its type. A call to it cannot be written
without deciding the failure’s fate — propagate, default, handle, or
deliberately discard. The reasons are values with names, so handling them is
case logic, not string matching. And cleanup registered with defer rides
the failure exit for free, so the arrangement has no bad day.
Next
The crossroads — part 7: one program that uses everything the numbered path taught, and the guided map into the themed tutorials where each piece becomes properly yours. Every example behind the series lives in the tutorial repository.