Error Handling

Tape uses error returns — functions that can fail return T or Error. No exceptions, no stack unwinding, no hidden control flow.

Basic pattern

tape
@profile(t1);
import io from "io";

enum ParseErr : u8 {
    BadInput = 1;
    Overflow = 2;
}

fn parse_positive(val: i64) -> i64 or ParseErr {
    if (val < 0) { return ParseErr.BadInput; }
    if (val > 1000000) { return ParseErr.Overflow; }
    return val * 2;
}

fn process(val: i64) -> i64 or ParseErr {
    var doubled: i64 = parse_positive(val) or return;
    return doubled + 1;
}

pub fn main() -> i32 {
    var result: i64 = process(21) or return 1;
    io.print_i64(result);
    io.print("\n");
    return 0;
}

How it works

A function declares that it can fail by using or ErrorType in its return type:

tape
fn parse_positive(val: i64) -> i64 or ParseErr {
    if (val < 0) { return ParseErr.BadInput; }
    return val * 2;
}

The caller must handle the error explicitly:

  • or return — propagate the error to the caller
  • or { block } — handle the error inline
tape
// Propagate (caller must also return or Error):
var result: i64 = parse_positive(val) or return;

// Handle inline:
var result: i64 = divide(10, 0) or {
    io.println("division failed");
    return 1;
};

// Log the error description:
var result: i64 = parse_positive(-1) or {
    io.println(ParseErr.desc(err));  // "invalid input"
    return 1;
};

Error enums

Errors in tape are enum values with optional description strings:

tape
enum ParseErr : u8 {
    BadInput = 1: "invalid input";
    Overflow = 2: "value too large";
}

The description string is available at runtime via the enum’s .desc() method.

Design principles

  • Zero cost on success — just a tag check on the return value
  • Visible at every call site — no hidden throw that skips your cleanup
  • Composableor return chains naturally through call stacks
  • No exceptions — no stack unwinding, no catch blocks, no surprise control flow

Last modified: