Option and Result

Every program can use two generic enums without declaring them:

enum Option[T] do
    SOME(T value)
    NONE
end

enum Result[T, E] do
    OK(T value)
    ERROR(E error)
end

Option is a value that may be missing, and Result is the value of an operation that can fail, or the error that says why it failed. Result[T] is Result[T, Error]; another error type, such as an enum of the ways a parser can fail, is written as the second type argument. A function returns them like any other value, and match makes the caller handle both cases, so a missing value or a failure cannot be overlooked:

func find_word(str[] words, str word) Option[int] do
    for i = 0 to len(words) - 1 do
        if words[i] == word then
            return Option.SOME(i)
        end
    end
    return Option.NONE
end

match parse_int(text) do
    when OK(value) then
        print(value * 2)
    when ERROR(message) then
        eprint(message)
end

The names Option, Result, and Error cannot be declared again.

Errors

Error, a struct of the prelude, describes what went wrong:

struct Error do
    str message
    str kind = "error"
    Error cause = null
    str file = ""
    int line = 0
end

kind tells errors apart for match, and cause is the error this one adds to. file and line say where it was made: Error("not found", kind: "not_found") fills them in with its own place. A str given where an Error is expected becomes one made there, so Result.ERROR("no '+' here") needs no constructor, and an Error used where a str is expected, or joined to one with +, is its text: its message followed by those of its causes, as e.to_text() gives and print shows. e.trace() gives each error of the chain on a line of its own with its place.

The built-ins that fail make errors with these kinds: parse_int and parse_float give "invalid" or "out_of_range", and try_read_text gives "not_found", "permission", "invalid" for text that is not UTF-8, or "io".

match try_read_text(path) do
    when OK(text) then
        print(len(text))
    when ERROR(e) then
        match e.kind do
            when "not_found" then
                print("no such file")
            else
                eprint(e)
        end
end

panic(message) is for states that should be impossible: it stops the program with a runtime error, panic: message, and the trace of where it was. A function may end with panic(...) or exit(...) where it would otherwise need a return.

catch_panic(body) runs the function body and gives "" when it ends, or the message of the runtime error that stopped it, a panic among them, as panic: message or array index out of bounds. The functions it was in are left, and their deferred code runs. It is for programs that run code they cannot trust to be right, as a compiler checking each statement of a program, or a server keeping one request's failure from the others; errors that are expected are Results.

str problem = catch_panic(func() do
    handle(request)
end)
if problem != "" then
    log.error("request failed", {"problem": problem})
end

Passing errors on with ?

In a function that returns a Result, value? is what an OK holds, and an ERROR makes the function return that error at once. In a function that returns an Option, it does the same with SOME and NONE:

// Reads "a+b" and adds the numbers, or says what was wrong.
func add_text(str text) Result[int] do
    int plus = find(text, "+")
    if plus < 0 then
        return Result.ERROR("no '+' in '" + text + "'")
    end
    int left = parse_int(trim(slice(text, 0, plus)))?
    int right = parse_int(trim(slice(text, plus + 1, len(text))))?
    return Result.OK(left + right)
end

A call with ? can stand alone as a statement when only its error matters, as in close_file(f)?.

value ? "message" passes on a new error with that message, whose cause is the error of value and whose kind is the same, so the caller learns what was being done as well as what failed:

int port = parse_int(text) ? f"reading the port from '{text}'"
// reading the port from 'x80': 'x80' is not an integer

With a message, ? also works on an Option in a function that returns a Result: a NONE becomes an error with that message. When the function's error type differs from the value's, ? converts the error with the function's error type's from, a function of the type such as func from(Error e) AppError, and it is an error when there is none.

In the program body, outside any function, an ERROR or NONE met by ? ends the program: the error's text, after the message if one is written, goes to standard error, and the program exits with status 1. Small programs and tools can use ? without a function around it:

str config = try_read_text("app.conf") ? "reading the settings"
int workers = parse_int(trim(config))?

? may be used in a declaration, an assignment's value, a return, a call statement, the condition of an if (not of an else if), the value of a match, and the collection of a for ... in. Since its check runs before the rest of the statement, a ? cannot come after and or or, inside a function written with =>, or after a value in the same expression that a call could change, such as items[0] + parse_int(text)?; store that value in a variable first.