Skip to content

Errors ​

Errors are values in Talor: there is no exception, no try/catch, no null. Absence is one variant, failure is another, and both flow through the type system.

Option<T>: absence ​

talor
let a: Option<i64> = Some(3);
let b: i64? = None;              // T? is sugar for Option<T>

Result<T, E>: failure ​

talor
fn parse(s: string): Result<i64, string> {
    if s == "" { return Err("empty"); }
    Ok(42)
}

Option<T> is Some(T) | None; Result<T, E> is Ok(T) | Err(E). Both are read by match like any variant.

?: propagate ​

talor
let v = parse(text)?;       // returns Err(e) from the enclosing function on Err
let w = a?;                 // returns None from an Option-returning function

? works on Option<T> inside a function returning Option<_>, and on Result<T, E1> inside a function returning Result<_, E2>. An Err leaves by the first of these that applies:

  1. E1 is E2: the error leaves as it is.
  2. The program declares impl Convert<E2> for E1 (Convert is in std.error): the error leaves as e.convert().
  3. E2 is a variant with exactly one case that holds an E1 and nothing else: the error leaves in that case.

Anything else is refused at the ?. A function whose body uses ? writes its return type.

talor
use std.error.{Convert};

struct ParseError { text: string }
variant AppError { Parse(ParseError), Limit(i64) }

struct Code { n: i64 }
impl Convert<AppError> for Code {
    fn convert(self): AppError { AppError.Limit(self.n) }
}

fn parse(s: string): Result<i64, ParseError> {
    if s == "" { return Err(ParseError { text: "empty input" }); }
    Ok(s.len())
}

fn check(n: i64): Result<i64, Code> {
    if n > 3 { Err(Code { n: n }) } else { Ok(n) }
}

fn load(s: string): Result<i64, AppError> {
    let n = parse(s)?;        // ParseError leaves as AppError.Parse
    let m = check(n)?;        // Code leaves through its Convert
    Ok(m)
}

fn main(): i32 {
    for input in ["ab", "", "abcdef"] {
        match load(input) {
            Ok(n) => println(`'${input}': ${n}`),
            Err(AppError.Parse(p)) => println(`'${input}': ${p.text}`),
            Err(AppError.Limit(k)) => println(`'${input}': ${k} is over the limit`),
        }
    }
    0
}

Error ​

The standard library's failures answer std.error's Error: e.code() is the operating system's error number, or the code of the failure, and e.message() is the text. Error.saying(text) makes one for a program's own failure.

Methods ​

is_some, is_none, unwrap, unwrap_or(d), is_ok, is_err, unwrap_err. A match covers the rest in two lines.

panic: what cannot continue ​

talor
panic("index out of bounds");

A panic writes PANIC: <what happened> on standard error, and under it the place and the task: at main.talor:3:15, on main. With TALOR_BACKTRACE=1 in the environment it also prints the frames between the panic and the start of the task, and with TALOR_BACKTRACE=full every frame.

Then it unwinds: every frame between the panic and the start of the task releases what it owns, and the task ends. On the main thread that ends the process with the code 101. In a fiber or a thread the program started, only that task ends, and whoever joins it gets Err with code 101, the panic's text, and e.panicked() true: see concurrency. There is no catch.

What panics: overflow and a shift past the width in a debug build, an index outside an array, unwrap on None, an allocation the operating system refused, a lock taken again by the task that holds it, and panic(msg) written by the program. The code is not 1, because 1 is what any process answers when it fails for a reason of its own, and a test that expects a panic would pass on an unrelated failure.

A stack overflow is not a panic: the guard page ends the program with the abort code 134, because there is no stack left to unwind or print from.

never ​

The type of an expression that does not return - panic, return, break - is never, which unifies with everything: the else branch of a value-producing if can be a panic without changing the type.

Talor v0.1.0 - Released under the MIT OR Apache-2.0 license.