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
| Form | Meaning |
|---|---|
T ! E | a success holding a T, or a failure holding an E |
! E | exactly () ! 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 !:
| Type | Means |
|---|---|
int32 | bool ! IoError | (int32 | bool) ! IoError |
int32 ! ParseError | IoError | int32 ! (ParseError | IoError) — an error sum |
int32? ! IoError | a fallible whose success is optional |
int32 ! (IoError?) | a fallible whose error is optional — the group is required |
(int32 ! IoError)? | an optional fallible |
(int32 ! ParseError) ! IoError | a 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:
| Write | Produces |
|---|---|
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 end | a 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 type | Use it when |
|---|---|
a struct | one kind of failure, with details: IoError { code: 28 } |
a variant | several named kinds, each with its own payload: DivideError::Inexact(1) |
an enum | several kinds with no payload |
a sum, A | B | the 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:
| Tool | Who decides what a failure means |
|---|---|
match on .Success / .Failure | this 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 returns | Exit status on success | On failure |
|---|---|---|
! E | 0 | 1 |
int ! E | the returned int | 1 |
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
- Handling failures —
match,catch, and the discard rules - Error propagation —
?,? else, and?? fail - Panics — failures that stop the program
- Optionals — absence, which is not an error
- Learn: Fallible, Fail, Unit fallible, Outcome, Error variant, Error sum, Nested fallible, Fallible main