Errors · Lesson 9.12

Fallible main

Source
Declare func Main() -> ! E so ? works at the top level, and see that a failure ends the program with exit status 1 without printing anything.
You'll need: Unit fallible, Propagate

Every program so far ended with return 0; from func Main() -> int. That made ? useless at the top level: Main could not fail, so a failure had nowhere to go, and every step had to be matched by hand.

Main may also be fallible. Then ? works at the top level too, and a failure ends the program.

Main may fail

The installer in this lesson reserves disk space for two programs. Reserve fails with a SpaceError when there is not enough room, and Main declares the same error:

func Main() -> ! SpaceError {
    var free = 100;
    PrintLine("Installing the editor (40 MB of {} MB free)", free);
    free = Reserve(40, free)?;

Main is now a unit fallible, so it follows the same rules: falling off its end is success, and it has no return 0;. The second reservation asks for 90 MB with only 60 free, so its ? ends the program — the last PrintLine never runs.

How the program ends

The operating system receives a number when a program ends, its exit status. 0 means success; anything else means something went wrong.

Main is declaredand ends byExit status
-> intreturn n;n
-> ! Ereaching its end0
-> int ! Ereturn n;n
-> ! E or -> int ! Efail or a failed ?1

A failure still runs the program's ordinary cleanup on the way out, just as a return would — unlike a panic, which stops at once.

A failure prints nothing

The surprise is what a failure does not do: it prints nothing. The SpaceError value is not shown anywhere, so a user only sees the program stop after the second line.

The exit status is for whoever started the program — a script, a build tool or a shell. In PowerShell, echo $LASTEXITCODE shows it right after the program ends; in a POSIX shell such as bash, it is echo $?. That 1 is the last line of the output below.

If a person should learn why the program stopped, print a message yourself before failing. A catch arm can do both — print, then fail with the same error:

free = Reserve(90, free) catch {
    e => {
        PrintLine("Not enough space: {} MB needed, {} MB free", e.needed, e.free);
        fail e;
    }
};

The arm e => ... binds the whole error, and its block ends by leaving, so it never has to produce a value.

The program

The whole lesson is one package in the Examples repository. Its comments explain every step.

Src/Main.rux
// Every program so far ended with `return 0;` from `func Main() -> int`. `Main`
// may also be fallible:
//
//     func Main() -> ! SpaceError
//
// Now `?` works at the top level too. Falling off the end of `Main` is
// success, and the program exits with status 0. A failure — from `fail` or
// from a `?` — ends the program with exit status 1.
//
// The surprise is what a failure does *not* do: it prints nothing. The error
// value is not shown anywhere, so a user only sees the program stop. If they
// should learn why, print a message yourself before failing. The exit status
// is for whoever started the program: a script, a build tool or a shell. In
// PowerShell, `echo $LASTEXITCODE` shows it right after the program ends.
//
// This program fails on purpose: the second install does not fit.
import Io::PrintLine;

struct SpaceError {
    needed: int;
    free: int;
}

func Reserve(needed: int, free: int) -> int ! SpaceError {
    if needed > free {
        fail SpaceError { needed: needed, free: free };
    }
    return free - needed;
}

func Main() -> ! SpaceError {
    var free = 100;
    PrintLine("Installing the editor (40 MB of {} MB free)", free);
    free = Reserve(40, free)?;

    PrintLine("Installing the compiler (90 MB of {} MB free)", free);
    free = Reserve(90, free)?;

    // Never printed: the `?` above ended the program with status 1.
    PrintLine("Everything is installed, {} MB left", free);
}

Run it

cd Examples/Errors/FallibleMain
rux run
echo $?

The program fails on purpose, so it stops after the second line, and echo $? prints its exit status, 1. In PowerShell, use echo $LASTEXITCODE instead.

Installing the editor (40 MB of 100 MB free)
Installing the compiler (90 MB of 60 MB free)
1

Common mistakes

Keeping return 0 in a fallible Main.
In func Main() -> ! SpaceError, return 0; is error: 'return' value must have type '! SpaceError', but found 'int'. Let Main reach its end — or declare -> int ! SpaceError if you want to choose the status yourself.
Expecting the error to be shown.
A failed Main exits with status 1 and prints nothing about the error. Print what the user needs to know before the failure, as the catch above does.

Try it yourself

  1. Change the compiler's 90 MB to 50 and run again. What does echo $LASTEXITCODE show now?
  2. Replace the second ? with the catch above, so the user learns why the install stopped.
  3. Declare func Main() -> int ! SpaceError, end it with return 2;, and check the exit status when everything fits.

Learn more