Chapter 04 8 min read

Errors as values

In Python, a failing function throws an exception — a hidden second return path that interrupts normal flow until something catches it. In Glide, a failing function returns a special value that the caller has to look at. Failure modes live in the type signature. Nothing is hidden.

This sounds like more work, and at first it is — but the payoff is that you can't accidentally ignore an error, and every place where things can go wrong is visible right in the source.

The two failure-shaped types

There are two types that encode "the thing might not be there":

  • `!T` — pronounce "bang T," meaning "a T, or an error message". Built with ok(value) or err("message").
  • `?T` — pronounce "maybe T," meaning "a T, or nothing". Built with some(value) or none().

Use !T when something failed for a reason worth explaining. Use ?T when there's just no value to return, and you don't need a reason.

fn parse_int(s: string) -> !i32 {
    if s.eq("") { return err("empty input"); }
    if !all_digits(s) { return err("not a number"); }
    return ok(/* the parsed value */ 42);
}

fn first_vowel(s: string) -> ?string {
    for let i: i32 = 0; i < s.len(); i = i + 1 {
        if is_vowel(s.at(i).to_int()) {
            return some(s.substring(i, i + 1));
        }
    }
    return none();
}

Reading the value out

Both types have two fields you can peek at: a flag and a payload.

For !T:

  • .ok is true when the operation succeeded
  • .val holds the value (only meaningful when .ok is true)
  • .err holds the message (only meaningful when .ok is false)
let r: !i32 = parse_int("42");
if r.ok {
    println!("parsed:", r.val);
} else {
    println!("error:", r.err);
}

For ?T:

  • .has is true when there's a value
  • .val holds it
let v: ?string = first_vowel("hello");
if v.has {
    println!("got:", v.val);
} else {
    println!("no vowel");
}

The ? operator

Manually checking .ok after every fallible call gets tedious fast:

fn pipeline(s: string) -> !i32 {
    let a: !i32 = parse_int(s);
    if !a.ok { return err(a.err); }   // bail
    let b: !i32 = parse_int(/* something else */);
    if !b.ok { return err(b.err); }   // bail again
    return ok(a.val + b.val);
}

The ? operator collapses that pattern to one character. Place it right after a !T value:

fn pipeline(s: string) -> !i32 {
    let a: i32 = parse_int(s)?;    // err → return err; ok → unwrap to a
    let b: i32 = parse_int(s)?;
    return ok(a + b);
}

What ? does, mechanically:

  1. If the value's .ok is false, return that error from the enclosing function.
  2. Otherwise, evaluate to the .val.

It only works inside a function that itself returns !T (or ?T, which has analogous semantics for none).

The ?? operator: defaults

Sometimes you don't care about the error — you just want a fallback value. ?? is the operator for that:

let port: i32 = parse_int(env_get("PORT")) ?? 8080;

If parse_int succeeds, port gets its value. If it fails, port gets 8080. No if, no match, no early-return.

?? also works on ?T:

let first: string = first_vowel(input) ?? "no vowels";

Constructing the values

The constructors are lowercase functions:

return ok(42);
return err("bad input");
return some("hi");
return none();

All four are lowercase calls. none() takes no argument because there's nothing to wrap; the other three take the thing they wrap.

defer and defer_err

When a function opens a resource (a file, a connection, an allocation) it usually needs to close it before returning — whether the function succeeded or not. Glide has two keywords for that.

`defer` schedules code to run when the enclosing function exits, no matter how (normal return, ? propagation, early return). It's how you guarantee cleanup:

fn process(path: string) -> !i32 {
    let f: *File = open(path)?;
    defer f.close();      // runs when this function returns, period

    let line: string = f.read_line()?;   // if this fails, f still gets closed
    return ok(line.len());
}

`defer_err` is the same idea but only fires on the error path — useful for compensating actions:

fn create_user(name: string) -> !User {
    let id: i64 = db_insert(name)?;
    defer_err db_delete(id);            // roll back ONLY if something below fails

    send_welcome_email(name)?;          // if this fails, the rollback runs
    return ok(User { id: id, name: name });
}

If send_welcome_email fails, defer_err runs and the partial insert gets rolled back. If everything succeeds, defer_err is skipped — the user stays.

Compared to Python's try/except

Take this Python:

python
def get_config(path):
    try:
        with open(path) as f:
            data = json.load(f)
        return data["port"]
    except FileNotFoundError:
        return 8080
    except json.JSONDecodeError as e:
        raise ValueError(f"bad JSON: {e}")

In Glide, the same logic looks like this:

import stdlib::fs::*;
import stdlib::json::*;

fn get_config(path: string) -> !i32 {
    let raw: string = fs_read_or_default(path, "");    // missing file → ""
    if raw.eq("") { return ok(8080); }                 // default port
    let cfg: *JsonValue = JsonValue::parse(raw);
    let port: i32 = cfg.get_int("port")?;              // missing/non-int key → bubble up
    return ok(port);
}

Notice:

  • fs_read_or_default collapses the "missing file" case to a fallback inline — no try/except.
  • cfg.get_int("port") returns !i32, so the ? propagates a "no such key" / "not an integer" error — the caller sees it as !i32 with that message.
  • The return type !i32 announces that this function can fail. Anyone calling it knows immediately.

When errors are truly unrecoverable

For genuinely impossible states ("the array is empty here, which my invariants forbid"), use panic! or assert! — they print a message + source location and abort the process:

let v: *Vector<i32> = Vector::new();
v.push_all!(1, 2, 3);
if v.len() == 0 {
    panic!("this can't happen — I just pushed three elements");
}

But save this for genuine "impossible" cases. Anything a user or the network can trigger should be !T and propagate normally.

Recap

Where to next

You can now write functions that fail safely. The next chapter — Memory model — explains the other half of Glide's safety story: how the language tracks who owns each piece of memory without a garbage collector, and without lifetime annotations like Rust.