Errors

Rux has no exceptions. A function that can fail says so in its return type with a fallible T ! E: a successful T or a failed E. The caller receives the failure as an ordinary value and has to deal with it — the compiler rejects code that drops it on the floor.

fallible-type = [ type ] "!" type
FormMeaning
T ! Ea success holding a T, or a failure holding an E
! Eexactly () ! E: successful completion with no value, or a failure

The two sides are called channels. They stay distinct even when T and E are the same type: an int32 ! int32 is either a successful 5 or a failed 5, never just 5.

Conditions that no caller could recover from — a broken invariant, a bug — are not errors in this sense. They stop the program with a panic.

The type

! is the loosest type operator. The postfix suffixes ?, [], [N] and [..] bind first, then | (a sum), then !:

TypeMeans
int32 | bool ! IoError(int32 | bool) ! IoError
int32 ! ParseError | IoErrorint32 ! (ParseError | IoError) — an error sum
int32? ! IoErrora fallible whose success is optional
int32 ! (IoError?)a fallible whose error is optional — the group is required
(int32 ! IoError)?an optional fallible
(int32 ! ParseError) ! IoErrora nested fallible — the group is required

A type holds at most one unparenthesized !:

error: a type contains at most one unparenthesized '!'
  help: group the nested fallible, as in '(T ! E) ! F' or 'T ! (E ! F)'

Producing a result

Inside a function that returns T ! E:

WriteProduces
return value;a success holding value
fail error;a failure holding error, and leaves the function
return;a success, when the success type is exactly ()
falling off the enda success, when the success type is exactly ()
variant DivideError {
    ByZero,
    Inexact(int)
}

func ExactDivide(a: int, b: int) -> int ! DivideError {
    if b == 0 {
        fail DivideError::ByZero;
    }
    if a % b != 0 {
        fail DivideError::Inexact(a % b);
    }
    return a / b;
}

func Save(ok: bool) -> ! IoError {
    if !ok {
        fail IoError { code: 28 };
    }
    PrintLine("saved");
}

return is the success channel and fail the failure channel, and each checks its own operand: return DivideError::ByZero; is error: 'return' value must have type 'int ! DivideError', but found 'DivideError', and fail 30; is error: 'fail' value must have type 'DivideError', but found 'int'. In a ! E function there is no value to return, so return 0; is error: 'return' value must have type '! IoError', but found 'int'.

fail is a statement, and it is only allowed in a function with an error channel:

error: 'fail' needs an enclosing fallible function, but this function returns 'int'
  help: declare the function's error channel, as in '-> T ! E' or '-> ! E'

fail leaves like return: the error is evaluated first, then deferred statements run and live locals are destroyed.

Only a success type of exactly () completes without a value. A function with no return type is a void function, not a unit-returning one, and a type that merely contains the unit, such as ()?, still needs a value: return; there is error: 'return' requires a value of type '()?'.

Constructing either channel

.Success(value) and .Failure(error) build a fallible as a value, without leaving the function. They can be stored, passed, compared and returned like anything else:

let accepted: int ! DivideError = .Success(3);
let rejected: int ! DivideError = .Failure(DivideError::ByZero);
let plain: int ! DivideError = 3;    // a plain value is a success
let unit: ! IoError = .Success(());  // the unit success
let same: int32 ! int32 = .Failure(5);

A plain value of the success type becomes a success wherever a fallible is expected, as plain shows; a failure always needs .Failure or fail. A constructor needs an expected type for whatever it does not fix itself:

error: cannot infer the type of 'promised' from a native constructor with an unknown channel

return .Failure(error); and fail error; produce the same result; fail is the usual spelling, and .Failure is for a failure that is stored rather than returned.

Error types

An error is an ordinary value of an ordinary type. There is no base error type, no error interface, and no implicit conversion between error types. Common choices:

Error typeUse it when
a structone kind of failure, with details: IoError { code: 28 }
a variantseveral named kinds, each with its own payload: DivideError::Inexact(1)
an enumseveral kinds with no payload
a sum, A | Bthe failures of several steps, each kept with its own type
()only the fact of failure matters: int32 ! (), fail ();

An error sum widens implicitly: a fail or ? with any member — or with a smaller sum of members — fits. A public function usually names a variant instead, so that its error does not change every time an internal step gains a failure; a type alias of a sum hides none of its members.

Reading the result

A fallible is not its success value: ExactDivide(12, 2) / 2 is error: operator '/' cannot combine left operand 'int ! DivideError' with right operand 'int'. The value inside is reached by one of these:

ToolWho decides what a failure means
match on .Success / .Failurethis function, case by case
catch { … }this function, with a recovery value
?the caller — the failure is passed on
? else (e => …)the caller, after the error is converted
func Show(outcome: int ! DivideError) {
    match outcome {
        .Success(value) => PrintLine("= {}", value),
        .Failure(DivideError::ByZero) => PrintLine("division by zero"),
        .Failure(DivideError::Inexact(rest)) => PrintLine("remainder {}", rest)
    }
}

A fallible cannot be ignored. Calling ExactDivide(12, 4); as a statement is an error; see Discarding a result.

Nesting

Levels never collapse. In a (int32 ! ParseError) ! IoError, a failed inner result held as a success is data — the outer operation worked, and what it produced is a failure — not a failure of the outer level:

func Layered(depth: int32) -> (int32 ! ParseError) ! IoError {
    if depth == 0 {
        return .Success(.Success(1));
    }
    if depth == 1 {
        return .Success(.Failure(ParseError { position: 2 }));
    }
    fail IoError { code: 3 };
}

?, catch and a .Success/.Failure pattern each handle exactly one level. Where a value could become either a success or a failure of the outer level, the compiler refuses to guess: with type R = int32 ! ParseError;, return value; in a function returning (int32 | R) ! ParseError could store value as successful data or forward its failure.

error: conversion from 'int32 ! ParseError' to '(int32 | (int32 ! ParseError)) ! ParseError' is ambiguous; write '.Success(...)', '.Some(...)', or '?' to choose one, or annotate an intermediate type

return .Success(value); stores it, and return value?; forwards it.

Equality

== and != compare the channel first, then the active payload with the payload's own ==. A success and a failure holding equal values are different, and two unit successes are equal.

A fallible Main

Main may return ! E or int ! E, so the top level of a program can use ?:

Main returnsExit status on successOn failure
! E01
int ! Ethe returned int1

A failure runs the ordinary cleanup — defers and destructors — and exits with status 1 without printing anything. Report what the user needs to know before failing.

No methods

A fallible is a compiler-owned form. It cannot be extended and implements no interface; operations on fallibles are generic free functions, such as Core::Succeeded and Core::Failed.

Layout

A fallible is stored as an 8-byte tag (success or failure) followed by the payload of the active channel, so sizeof(int32 ! int64) is 16. A zero-sized payload takes no room: a ! E is the tag and an E, and sizeof(! ()) is 8. The tag values are not a stable ABI. See Layout.

See also