# Chapter 12: Error Handling

**What you'll learn.** Loup's two error mechanisms, exceptions and `Result[T, E]`; how to throw, catch, chain, and define exceptions; what an uncaught exception prints; how to handle and propagate a `Result` with `match`, `?`, and `attempt`; how to guarantee cleanup; and how errors behave in `async` code on the current toolchain.

**Why it matters.** Most bugs that reach users are error paths nobody exercised: a form field that parses to `0` instead of failing, a spinner that never hides, a network error that escapes into an "Uncaught" message. Loup gives you explicit tools for each case, and this chapter shows the mistakes you are likely to make with them and what the compiler or runtime says when you do.

Loup has two ways to report failure:

- **Exceptions** are thrown with `throw` and caught with `try`/`catch`. An exception nobody catches unwinds the whole program. Use them for failures the immediate caller usually cannot fix: a broken invariant, an index out of range, a server that is down.
- **`Result[T, E]`** is an ordinary enum, `Ok(value)` or `Err(error)`, returned like any other value. Use it for failures the caller is expected to handle: invalid user input, a missing field, a value out of range.

Both are available in both web modes. The examples use `@mode(web.compiled)`, where `loup run` calls `main()`.

## Throwing and catching

Listing 12-1 models a bank account whose `withdraw` method throws two kinds of exception, and a caller that handles each.

```loup
@mode(web.compiled)

class InsufficientFundsError : Exception {
    pub let requested: int;
    pub let available: int;

    new(requested: int, available: int) {
        super(i"insufficient funds: requested {$ requested $}, available {$ available $}");
        this.requested = requested;
        this.available = available;
    }
}

class Account {
    pub var balance: int;

    new(balance: int) {
        this.balance = balance;
    }

    pub modifier func withdraw(amount: int): void {
        if (amount <= 0) {
            throw ValueError(i"amount must be positive, got {$ amount $}");
        }
        if (amount > this.balance) {
            throw InsufficientFundsError(amount, this.balance);
        }
        this.balance = this.balance - amount;
    }
}

func tryWithdraw(edits account: Account, amount: int): string {
    try {
        safe { account.withdraw(amount); }
        return i"ok, balance is now {$ account.balance $}";
    } catch (InsufficientFundsError as e) {
        return i"short by {$ e.requested - e.available $}";
    } catch (ValueError as e) {
        return i"bad input: {$ e.message $}";
    } finally {
        println(i"attempted to withdraw {$ amount $}");
    }
}

func main(): void {
    let acct = Account(100);
    println(tryWithdraw(edits: acct, amount = 30));
    println(tryWithdraw(edits: acct, amount = 500));
    println(tryWithdraw(edits: acct, amount = -5));
}
```

*Listing 12-1: Throwing, catching by type, and a `finally` block*

```text
attempted to withdraw 30
ok, balance is now 70
attempted to withdraw 500
short by 430
attempted to withdraw -5
bad input: amount must be positive, got -5
```

The pieces:

- `throw ValueError("…")` constructs an exception and unwinds until a matching `catch`. You can only throw values whose class extends `Exception` (or `BaseException`); `throw "disk full"` is rejected with `E_THROW_NON_ERROR`.
- `catch (InsufficientFundsError as e)` names the type first and the binding second. Clauses are tried top to bottom, and a clause matches subclasses too, so put the most specific type first. One clause can name several types: `catch (IndexError | KeyError as e)`.
- `finally` runs on every exit from the `try`: normal completion, a `return` inside the `try`, a caught exception, or one that keeps propagating. The `finally` output appears before each result above because it runs before `tryWithdraw` actually returns.
- Every exception has a `message: string`, a `cause: Option[Any]`, and a `stackTrace` (Language Reference §1-18.1).

### Problem: the catch clause in TypeScript or Kotlin order

If you write the binding first, as in TypeScript's `catch (e: Error)`, the parser stops at the colon:

```loup,does_not_compile
@mode(web.compiled)

func readPort(raw: string): int {
    let n = raw.to_int();
    if (n <= 0 or n > 65535) {
        throw ValueError(i"port out of range: '{$ raw $}'");
    }
    return n;
}

func main(): void {
    try {
        println(readPort("99999"));
    } catch (e: ValueError) {
        println(e.message);
    }
}
```

```text
main.lpweb:14:15: error[E0001]: expected RParen, found Colon
main.lpweb:14:15: error[E0002]: expected `{`
main.lpweb:14:15: error[E0001]: expected expression
main.lpweb:14:17: error[E0003]: expected `;`
main.lpweb:17:1: error[E0001]: expected expression
```

Only the first line matters; the rest is the parser losing its place. Loup's catch clause is `catch (Type as name)`, like Python's `except Type as name`:

```loup
#~ @mode(web.compiled)
#~
#~ func readPort(raw: string): int {
#~     let n = raw.to_int();
#~     if (n <= 0 or n > 65535) {
#~         throw ValueError(i"port out of range: '{$ raw $}'");
#~     }
#~     return n;
#~ }
#~
#~ func main(): void {
try {
    println(readPort("99999"));
} catch (ValueError as e) {
    println(e.message);
}
#~ }
```

```text
port out of range: '99999'
```

## The built-in exceptions

These exception classes are built in and need no import (Language Reference §1-18.1). All of them extend `Exception`:

| Class | Raised by |
|---|---|
| `ValueError` | your own code, for a value that is invalid for the operation |
| `TypeError` | a failed runtime type check (`as`) |
| `IndexError` | `xs[i]` with `i` out of range |
| `KeyError` | `d[key]` with a missing key |
| `UnwrapError` | `unwrap()` or `expect()` on `None` or `Err` |
| `IntegerDivisionError` | integer `/`, `//`, or `%` by zero |
| `RenderError` | an `Err` value reaching `println`, `str`, or an interpolation |
| `IoError` | file and process operations (host code) |
| `RuntimeError` | faults in the runtime environment; `StackOverflowError` extends it |
| `TimeoutError`, `CancellationError` | timeouts and cancelled tasks |

Some failures you might expect to throw do not, in Loup 1.25.1:

- **Integer overflow wraps.** `9223372036854775807 + 1` evaluates to `-9223372036854775808`. The Language Reference specifies checked arithmetic (`NumericOverflowError`), but no lane implements it yet; check ranges yourself where it matters.
- **`"abc".to_int()` returns `0`** rather than failing. Validate the characters before converting (the worked example at the end does).
- **A negative index counts from the end**: `[10, 20, 30][-1]` is `30`, not an `IndexError`.

There is no built-in `IOError` (it is `IoError`), `FileNotFoundError`, `NetworkError`, or `ParseError`. Declare the ones your program needs.

### Problem: throwing an exception class that does not exist

A CSV-style record parser that throws `ParseError`, as it would in Python or Java:

```loup,does_not_compile
@mode(web.compiled)

func parseRecord(line: string): list[string] {
    let fields = line.split(",");
    if (fields.len() != 3) {
        throw ParseError(i"expected 3 fields, got {$ fields.len() $}");
    }
    return fields;
}

func main(): void {
    try {
        println(parseRecord("ada,36"));
    } catch (ParseError as e) {
        println(i"skipping line: {$ e.message $}");
    }
}
```

```text
main.lpweb:6:15: error[E0001]: undefined identifier `ParseError`
```

Define it. A user exception extends `Exception` or one of the built-in classes; here `ValueError` is the natural parent, so a handler for `ValueError` also catches parse errors. A constructor that takes extra data passes the message up with `super(message)`:

```loup
@mode(web.compiled)

class ParseError : ValueError {
    pub let line: int;

    new(message: string, line: int) {
        super(message);
        this.line = line;
    }
}

func parseRecord(text: string, line: int): list[string] {
    let fields = text.split(",");
    if (fields.len() != 3) {
        throw ParseError(i"expected 3 fields, got {$ fields.len() $}", line);
    }
    return fields;
}

func main(): void {
    let rows = ["ada,36,admin", "lin,41", "sam,29,editor"];
    var line = 1;
    for (row in rows) {
        try {
            println(parseRecord(row, line));
        } catch (ParseError as e) {
            println(i"line {$ e.line $}: {$ e.message $}");
        }
        line = line + 1;
    }
}
```

*Listing 12-2: A user-defined exception with an extra field*

```text
["ada", "36", "admin"]
line 2: expected 3 fields, got 2
["sam", "29", "editor"]
```

## Uncaught exceptions

### Problem: reaching for `panic`

Programmers coming from Rust or Go often write `panic(...)` for "this cannot happen":

```loup,does_not_compile
@mode(web.compiled)

func checkedIndex(xs: list[int], i: int): int {
    if (i < 0 or i >= xs.len()) {
        panic("index out of range");
    }
    return xs[i];
}

func main(): void {
    println(checkedIndex([10, 20, 30], 1));
}
```

```text
main.lpweb:5:9: error[E0001]: undefined identifier `panic`
```

The Language Reference lists `panic(msg)` in the prelude of every mode, but the Loup 1.25.1 compiler does not define it for hosted or web code (only the freestanding toolchain has a panic path). Throw an exception instead. If nothing catches it, the program stops, which is what you wanted from `panic`:

```loup,throws
@mode(web.compiled)

func checkedIndex(xs: list[int], i: int): int {
    if (i < 0 or i >= xs.len()) {
        throw IndexError(i"index {$ i $} out of range for {$ xs.len() $} items");
    }
    return xs[i];
}

func main(): void {
    println(checkedIndex([10, 20, 30], 1));
    println(checkedIndex([10, 20, 30], 3));
}
```

*Listing 12-3: An exception that nobody catches*

```text
20
Uncaught IndexError: index 3 out of range for 3 items
  at checkedIndex (main.lpweb:5:15)
  at main (main.lpweb:12:13)
```

The first call prints `20`. The second throws, nothing catches it, and the runner prints `Uncaught`, the class, the message, and one `at function (file:line:column)` line per frame, innermost first, on standard error. The exit status is 1. Read the trace bottom-up to see how the program got there: `main` called `checkedIndex` at line 12, which threw at line 5.

If you really mean "this point is impossible", Loup also has the `unreachable` expression; reaching it stops the program with ``Uncaught RuntimeError: panic: reached an `unreachable` expression``, and unlike an exception it cannot be caught, not even by `catch (Exception as e)`.

### Chaining a cause

When you catch a low-level exception and throw a higher-level one, attach the original as the cause with `from`. The trace then shows both:

```loup,throws
@mode(web.compiled)

class ConfigError : Exception {
    new(message: string) {
        super(message);
    }
}

func readPort(raw: string): int {
    let n = raw.to_int();
    if (n <= 0 or n > 65535) {
        throw ValueError(i"port out of range: '{$ raw $}'");
    }
    return n;
}

func loadConfig(raw: string): int {
    try {
        return readPort(raw);
    } catch (ValueError as e) {
        throw ConfigError("could not load config") from e;
    }
}

func main(): void {
    println(loadConfig("8080"));
    println(loadConfig("99999"));
}
```

*Listing 12-4: `throw … from e` records the cause*

```text
8080
Uncaught ConfigError: could not load config
  at loadConfig (main.lpweb:21:15)
  at main (main.lpweb:27:13)

Caused by:
Uncaught ValueError: port out of range: '99999'
  at readPort (main.lpweb:12:15)
  at loadConfig (main.lpweb:19:16)
  at main (main.lpweb:27:13)
```

A handler can read the chain through `e.cause`, an `Option`.

## `Result[T, E]`

For failures the caller should handle, return a `Result`. `Ok`, `Err`, `Some`, and `None` are built in. Listing 12-5 validates an email address and handles every outcome with `match`, which must cover both variants.

```loup
@mode(web.compiled)

func parseEmail(raw: string): Result[string, string] {
    let email = raw.trim().to_lowercase();
    if (not email.contains("@")) {
        return Err("missing '@'");
    }
    let parts = email.split("@");
    if (parts.len() != 2) {
        return Err("more than one '@'");
    }
    if (parts[0].len() == 0) {
        return Err("nothing before '@'");
    }
    if (not parts[1].contains(".")) {
        return Err(i"'{$ parts[1] $}' is not a domain");
    }
    return Ok(email);
}

func main(): void {
    for (raw in ["  Ada@Example.com ", "ada.example.com", "a@b@c.com", "@example.com", "ada@localhost"]) {
        match parseEmail(raw) {
            Ok(email) => println(i"ok:  {$ email $}"),
            Err(reason) => println(i"bad: {$ reason $}"),
        }
    }
    println(parseEmail("lin@example.org").is_ok());
    println(parseEmail("nope").unwrap_or("guest@example.org"));
    println(repr(parseEmail("x").map(func(e: string): int => e.len())));
}
```

*Listing 12-5: Returning and matching a `Result`*

```text
ok:  ada@example.com
bad: missing '@'
bad: more than one '@'
bad: nothing before '@'
bad: 'localhost' is not a domain
true
guest@example.org
Err("missing '@'")
```

The methods that work on `Result` and `Option` in Loup 1.25.1 are spelled in snake_case:

| Method | On `Result[T, E]` | On `Option[T]` |
|---|---|---|
| `is_ok()` / `is_err()` | `true` for `Ok` / `Err` | — |
| `is_some()` / `is_none()` | — | `true` for `Some` / `None` |
| `unwrap()` | the value, or throws `UnwrapError` | the value, or throws `UnwrapError` |
| `expect(msg)` | like `unwrap`, with your message | like `unwrap`, with your message |
| `unwrap_or(fallback)` | the value, or `fallback` | the value, or `fallback` |
| `unwrap_err()` | the error | — |
| `map(f)` | transforms the `Ok` value | transforms the `Some` value |
| `map_err(f)` | transforms the `Err` value | — |
| `and_then(f)` | chains a function returning `Result` | chains a function returning `Option` |
| `filter(pred)` | — | `None` unless `pred` holds |

For an `Option`, `value ?? fallback` is a shorter `unwrap_or`.

### Problem: the camelCase spelling from the Language Reference

The Language Reference writes these methods in camelCase (`unwrapOr`, `isSome`). Some of those spellings get through the checker and fail only when the line runs:

```loup,throws
@mode(web.compiled)

func main(): void {
    let r: Result[int, string] = Err("timeout");
    println(r.unwrapOr(0));
}
```

```text
Uncaught RuntimeError: internal interpreter limitation: unsupported instruction: CallVirtual unwrapOr on user class Result — method not found
```

"Internal interpreter limitation" means the program reached something the interpreter cannot execute; here, a method that does not exist. Use the snake_case name:

```loup
#~ @mode(web.compiled)
#~
#~ func main(): void {
let r: Result[int, string] = Err("timeout");
println(r.unwrap_or(0));
#~ }
```

```text
0
```

### Problem: printing a `Result` that might be an `Err`

Printing an `Ok` prints its value, so it is tempting to print a `Result` directly. An `Err` cannot be rendered for users; it throws:

```loup,throws
@mode(web.compiled)

func main(): void {
    let r: Result[int, string] = Err("timeout");
    println(r);
}
```

```text
Uncaught RenderError: rendered an `Err` value: Err("timeout")
  at main (main.lpweb:5:5)
```

This is deliberate (Language Reference §1-16.4): every text sink, including `println`, `str`, interpolation, and template fences, refuses to turn an unhandled error into text that would end up on a page. Handle the `Err` with `match` first, or use `repr(r)` when you want to look at the value while debugging; `repr` never throws.

```loup
#~ @mode(web.compiled)
#~
#~ func main(): void {
let r: Result[int, string] = Err("timeout");
println(repr(r));
match r {
    Ok(n) => println(n),
    Err(reason) => println(i"failed: {$ reason $}"),
}
#~ }
```

```text
Err("timeout")
failed: timeout
```

### Problem: a parse that never fails

`to_int()` returns `0` for text that is not a number. A quantity field validated only by range accepts garbage:

```loup,not_desired_behavior
@mode(web.compiled)

func parseQuantity(raw: string): Result[int, string] {
    let n = raw.to_int();
    if (n < 0 or n > 99) {
        return Err(i"{$ n $} is outside 0-99");
    }
    return Ok(n);
}

func main(): void {
    println(repr(parseQuantity("3")));
    println(repr(parseQuantity("ten")));
}
```

```text
Ok(3)
Ok(0)
```

`"ten"` became an order for zero items. Check that the text is a number before converting:

```loup
@mode(web.compiled)

func parseQuantity(raw: string): Result[int, string] {
    if (raw.len() == 0) {
        return Err("empty");
    }
    for (c in raw.chars()) {
        if (c < "0" or c > "9") {
            return Err(i"'{$ raw $}' is not a whole number");
        }
    }
    let n = raw.to_int();
    if (n > 99) {
        return Err(i"{$ n $} is outside 0-99");
    }
    return Ok(n);
}

func main(): void {
    println(repr(parseQuantity("3")));
    println(repr(parseQuantity("ten")));
}
```

*Listing 12-6: Validating digits before `to_int()`*

```text
Ok(3)
Err("'ten' is not a whole number")
```

## Propagating with `?`

Matching every `Result` by hand gets long when one function calls several fallible ones. The postfix `?` operator unwraps an `Ok` and, on an `Err`, returns that `Err` from the enclosing function at once (Language Reference §1-5.7). It may only be used inside a function that itself returns a `Result`.

```loup
@mode(web.compiled)

func parseHost(raw: string): Result[string, string] {
    if (raw.len() == 0) {
        return Err("host is empty");
    }
    return Ok(raw);
}

func parsePort(raw: string): Result[int, string] {
    let n = raw.to_int();
    if (n <= 0 or n > 65535) {
        return Err(i"bad port '{$ raw $}'");
    }
    return Ok(n);
}

func parseAddress(text: string): Result[string, string] {
    let parts = text.split(":");
    if (parts.len() != 2) {
        return Err(i"expected host:port, got '{$ text $}'");
    }
    let host = parseHost(parts[0])?;
    let port = parsePort(parts[1])?;
    return Ok(i"{$ host $} on port {$ port $}");
}

func main(): void {
    for (text in ["api.example.com:443", "api.example.com", ":80", "localhost:http"]) {
        match parseAddress(text) {
            Ok(addr) => println(addr),
            Err(reason) => println(i"error: {$ reason $}"),
        }
    }
}
```

*Listing 12-7: `?` returns the first `Err` to the caller*

```text
api.example.com on port 443
error: expected host:port, got 'api.example.com'
error: host is empty
error: bad port 'http'
```

### Problem: `?` with a different error type

`?` returns the `Err` unchanged, so its error type must match the function's. Here `parseAge` fails with a `string`, but `checkAge` promises a `FieldError`:

```loup,does_not_compile
@mode(web.compiled)

enum FieldError {
    Missing(field: string),
    Invalid(field: string, reason: string),
}

func parseAge(raw: string): Result[int, string] {
    if (raw.len() == 0) {
        return Err("empty");
    }
    return Ok(raw.to_int());
}

func checkAge(raw: string): Result[int, FieldError] {
    let age = parseAge(raw)?;
    return Ok(age);
}

func main(): void {
    println(repr(checkAge("")));
}
```

```text
main.lpweb:16:15: error[E_PROPAGATE_MISSING_FROM]: E_PROPAGATE_MISSING_FROM: '?' propagates an error of type 'string' out of a function whose error type is 'FieldError', which does not implement `From[string]` (§1-5.7)
```

The message names both types and the general mechanism: an error type that implements `From[string]` would be converted automatically. For a one-off conversion, convert the error yourself with `map_err` before applying `?`:

```loup
#~ @mode(web.compiled)
#~
#~ enum FieldError {
#~     Missing(field: string),
#~     Invalid(field: string, reason: string),
#~ }
#~
#~ func parseAge(raw: string): Result[int, string] {
#~     if (raw.len() == 0) {
#~         return Err("empty");
#~     }
#~     return Ok(raw.to_int());
#~ }
#~
func checkAge(raw: string): Result[int, FieldError] {
    let age = parseAge(raw).map_err(func(r: string): FieldError => FieldError.Invalid(field = "age", reason = r))?;
    return Ok(age);
}

func main(): void {
    match checkAge("") {
        Ok(age) => println(age),
        Err(FieldError.Missing(field)) => println(i"{$ field $} is missing"),
        Err(FieldError.Invalid(field, reason)) => println(i"{$ field $}: {$ reason $}"),
    }
}
```

```text
age: empty
```

### Problem: `?` on an `Option`

`?` is for `Result`. The Language Reference makes `?` on an `Option` a compile error (`E_PROPAGATE_NOT_RESULT`). Loup 1.25.1 accepts it inside a `Result`-returning function and produces an error value nobody asked for:

```loup,not_desired_behavior
@mode(web.compiled)

func findAge(ages: dict[string, int], name: string): Option[int] {
    if (ages.contains_key(name)) {
        return Some(ages[name]);
    }
    return None;
}

func ageNextYear(ages: dict[string, int], name: string): Result[int, string] {
    let age = findAge(ages, name)?;
    return Ok(age + 1);
}

func main(): void {
    let ages: dict[string, int] = {"ada": 36};
    println(repr(ageNextYear(ages, "ada")));
    println(repr(ageNextYear(ages, "bob")));
}
```

```text
Ok(37)
Err(None)
```

The function promised a `string` error and returned `Err(None)`. (Inside a function that returns `Option`, the same `?` is rejected with `E_PROPAGATE_WRONG_RETURN`.) Say what a missing value means by matching on the `Option`:

```loup
#~ @mode(web.compiled)
#~
#~ func findAge(ages: dict[string, int], name: string): Option[int] {
#~     if (ages.contains_key(name)) {
#~         return Some(ages[name]);
#~     }
#~     return None;
#~ }
#~
func ageNextYear(ages: dict[string, int], name: string): Result[int, string] {
    let age = match findAge(ages, name) {
        Some(a) => a,
        None => {
            return Err(i"no age on file for {$ name $}");
        },
    };
    return Ok(age + 1);
}

func main(): void {
    let ages: dict[string, int] = {"ada": 36};
    println(repr(ageNextYear(ages, "ada")));
    println(repr(ageNextYear(ages, "bob")));
}
```

```text
Ok(37)
Err("no age on file for bob")
```

## `attempt`: handling a `Result` in a `catch` arm

`?` always returns from the function. Sometimes you want to recover in place instead, for example to fall back to a default. The `attempt` prefix operator unwraps an `Ok`; on an `Err`, it jumps to the `catch Err(e)` arm of the enclosing `try` (Language Reference §1-5.10):

```loup
@mode(web.compiled)

func parsePort(raw: string): Result[int, string] {
    let n = raw.to_int();
    if (n <= 0 or n > 65535) {
        return Err(i"bad port '{$ raw $}'");
    }
    return Ok(n);
}

func main(): void {
    var port = 0;
    try {
        port = attempt parsePort("80x");
    } catch Err(e) {
        println(i"using default: {$ e $}");
        port = 8080;
    }
    println(port);
}
```

*Listing 12-8: `attempt` sends an `Err` to a `catch Err` arm*

```text
using default: bad port '80x'
8080
```

`attempt` needs that arm. Without an enclosing `try`, the compiler has nowhere to send the error:

```loup,does_not_compile
@mode(web.compiled)

func parsePort(raw: string): Result[int, string] {
    return Ok(raw.to_int());
}

func main(): void {
    let port = attempt parsePort("80");
    println(port);
}
```

```text
main.lpweb:8:16: error[E_ATTEMPT_OUTSIDE_BLOCK]: 'attempt' requires an enclosing 'try' block with a 'catch Err(e)' arm
```

Wrap the code in `try { … } catch Err(e) { … }` as in Listing 12-8, or use `?` if the function returns a `Result` and the caller should decide.

## Converting between exceptions and `Result`

The two mechanisms meet at boundaries. To turn a `Result` into an exception, match and throw; to turn an exception into a `Result`, catch and return `Err`:

```loup
@mode(web.compiled)

func parsePort(raw: string): Result[int, string] {
    let n = raw.to_int();
    if (n <= 0 or n > 65535) {
        return Err(i"bad port '{$ raw $}'");
    }
    return Ok(n);
}

func requirePort(raw: string): int {
    return match parsePort(raw) {
        Ok(n) => n,
        Err(reason) => {
            throw ValueError(reason);
        },
    };
}

func safeLookup(rows: list[string], i: int): Result[string, string] {
    try {
        return Ok(rows[i]);
    } catch (IndexError as e) {
        return Err(e.message);
    }
}

func main(): void {
    println(requirePort("443"));
    try {
        println(requirePort("0"));
    } catch (ValueError as e) {
        println(i"thrown: {$ e.message $}");
    }
    println(repr(safeLookup(["a", "b"], 1)));
    println(repr(safeLookup(["a", "b"], 5)));
}
```

*Listing 12-9: Crossing between `Result` and exceptions*

```text
443
thrown: bad port '0'
Ok("b")
Err("list index 5 out of range (len 2)")
```

A useful rule: validate input with `Result` close to where it arrives, and throw for infrastructure failures that most callers cannot do anything about. Whichever split you choose, be consistent within a module.

## Cleanup that always runs

Loading indicators, open handles, and locks must be released however a function exits. `finally` does that:

```loup
@mode(web.compiled)

class Spinner {
    pub var visible: bool = false;

    pub modifier func show(): void {
        this.visible = true;
        println("spinner on");
    }

    pub modifier func hide(): void {
        this.visible = false;
        println("spinner off");
    }
}

safe func save(edits spinner: Spinner, payload: string): int {
    spinner.show();
    try {
        if (payload.len() == 0) {
            throw ValueError("nothing to save");
        }
        println(i"saved {$ payload.len() $} bytes");
        return payload.len();
    } finally {
        spinner.hide();
    }
}

func main(): void {
    let spinner = Spinner();
    safe {
        save(edits: spinner, payload = "hello");
        try {
            save(edits: spinner, payload = "");
        } catch (ValueError as e) {
            println(i"error: {$ e.message $}");
        }
    }
    println(spinner.visible);
}
```

*Listing 12-10: `finally` hides the spinner on success and on failure*

```text
spinner on
saved 5 bytes
spinner off
spinner on
spinner off
error: nothing to save
false
```

### Known gap: `defer` does not run yet

The Language Reference also has `defer`, which registers a statement or block to run when the enclosing block exits, by any path, in last-in-first-out order (§1-6.5). It is the tidier tool when a function acquires several resources. Loup 1.25.1 parses `defer` but never runs the deferred code:

```loup,not_desired_behavior
@mode(web.compiled)

func work(fail: bool): void {
    println("open");
    defer {
        println("close");
    }
    if (fail) {
        throw ValueError("boom");
    }
    println("work done");
}

func main(): void {
    work(false);
    try {
        work(true);
    } catch (ValueError as e) {
        println(e.message);
    }
}
```

```text
open
work done
open
boom
```

`close` should appear after `work done` and before `boom`; it never appears. Until this is fixed, write cleanup with `try`/`finally` as in Listing 12-10.

## Errors in async code

`try`/`catch`, `finally`, and `?` work in `async` functions as in ordinary ones. To `await` at all, the caller must itself be `async`; that includes `main`:

```loup,does_not_compile
@mode(web.compiled)

async func fetchStatus(path: string): Task[int] {
    if (path == "/missing") {
        throw ValueError("404 not found");
    }
    return 200;
}

func main(): void {
    println(await fetchStatus("/"));
}
```

```text
main.lpweb:11:13: error[E_AWAIT_NOT_ASYNC]: E_AWAIT_NOT_ASYNC: `await` is only valid inside an `async func` body, an entry script's top level, or the REPL (§2-1.1)
```

Declare `async func main(): Task[void]`; the runner waits for it. Listing 12-11 catches a failure from an awaited call and propagates a `Result` with `?` through an `async` function.

```loup
@mode(web.compiled)

class HttpError : Exception {
    pub let status: int;

    new(status: int) {
        super(i"server answered {$ status $}");
        this.status = status;
    }
}

async func fetchStatus(path: string): Task[int] {
    if (path == "/missing") {
        throw HttpError(404);
    }
    return 200;
}

async func loadPage(path: string): Task[string] {
    try {
        let status = await fetchStatus(path);
        return i"loaded {$ path $} ({$ status $})";
    } catch (HttpError as e) {
        return i"fallback page: {$ e.message $}";
    }
}

async func parseCount(raw: string): Task[Result[int, string]] {
    if (raw.len() == 0) {
        return Err("empty input");
    }
    return Ok(raw.to_int());
}

async func doubled(raw: string): Result[int, string] {
    let n = (await parseCount(raw))?;
    return Ok(n * 2);
}

async func main(): Task[void] {
    println(await loadPage("/home"));
    println(await loadPage("/missing"));
    println(repr(await doubled("21")));
    println(repr(await doubled("")));
}
```

*Listing 12-11: Exceptions and `?` in `async` functions*

```text
loaded /home (200)
fallback page: server answered 404
Ok(42)
Err("empty input")
```

Two things about the current toolchain:

- `doubled` declares its return type as `Result[int, string]`; an `async func` with a plain return type means the same as `Task[Result[int, string]]` (Language Reference §2-1.1). Use that form when the body uses `?`: Loup 1.25.1 rejects `?` in a function spelled `: Task[Result[…]]` with `E_PROPAGATE_WRONG_RETURN`, although the Language Reference allows it.
- On the 1.25.1 interpreter, `async` code runs eagerly: calling an `async` function runs its whole body before the call returns, and there is no scheduler (Chapter 15). That changes *where* a failure surfaces, as the next problem shows.

### Problem: the task is created outside the `try`

The Language Reference says that a failed task rethrows its failure at `await`. Code that starts a task early and awaits it inside a `try` relies on that:

```loup,throws
@mode(web.compiled)

async func fetchStatus(path: string): Task[int] {
    println(i"fetching {$ path $}");
    if (path == "/missing") {
        throw ValueError("404");
    }
    return 200;
}

async func main(): Task[void] {
    let task = fetchStatus("/missing");
    println("task created");
    try {
        let status = await task;
        println(status);
    } catch (ValueError as e) {
        println(i"caught at await: {$ e.message $}");
    }
}
```

```text
fetching /missing
Uncaught ValueError: 404
  at fetchStatus (main.lpweb:6:15)
  at main (main.lpweb:12:16)
```

Because the body ran during the call on line 12, the exception escaped there, outside the `try`, and `task created` was never printed. Put the call inside the `try` together with its `await`, as `loadPage` does in Listing 12-11; that code is correct both today and once a real scheduler arrives:

```loup
#~ @mode(web.compiled)
#~
#~ async func fetchStatus(path: string): Task[int] {
#~     println(i"fetching {$ path $}");
#~     if (path == "/missing") {
#~         throw ValueError("404");
#~     }
#~     return 200;
#~ }
#~
async func main(): Task[void] {
    try {
        let status = await fetchStatus("/missing");
        println(status);
    } catch (ValueError as e) {
        println(i"caught: {$ e.message $}");
    }
}
```

```text
fetching /missing
caught: 404
```

> ⚠ **Planned.** The Language Reference also describes how errors cross the browser boundaries: exceptions from Web Workers arriving as failed tasks, JavaScript exceptions surfacing as `JsException`, and Loup exceptions thrown out of `@jsexport` functions becoming JavaScript `Error`s. None of the browser runtime exists in Loup 1.25.1; Chapter 18 describes the design.

## A worked example: a signup form

Listing 12-12 validates a signup form and then calls a (simulated) server. Validation failures are expected, so they are a `Result` with an enum of reasons the page can turn into messages. The server call can fail for reasons the form cannot fix, so it throws.

```loup
@mode(web.compiled)

struct SignupForm {
    email: string;
    age: string;
    agreed: bool;
}

struct Signup {
    email: string;
    age: int;
}

enum FieldError {
    Missing(field: string),
    Invalid(field: string, reason: string),
    NotAgreed,
}

class HttpError : Exception {
    pub let status: int;

    new(status: int) {
        super(i"server answered {$ status $}");
        this.status = status;
    }
}

func parseEmail(raw: string): Result[string, string] {
    let email = raw.trim().to_lowercase();
    let parts = email.split("@");
    if (parts.len() != 2 or parts[0].len() == 0) {
        return Err("expected one name before '@'");
    }
    if (not parts[1].contains(".")) {
        return Err(i"'{$ parts[1] $}' is not a domain");
    }
    return Ok(email);
}

func parseAge(raw: string): Result[int, string] {
    if (raw.len() == 0) {
        return Err("empty");
    }
    for (c in raw.chars()) {
        if (c < "0" or c > "9") {
            return Err(i"'{$ raw $}' is not a whole number");
        }
    }
    let age = raw.to_int();
    if (age < 13 or age > 120) {
        return Err(i"{$ age $} is outside 13-120");
    }
    return Ok(age);
}

func validate(form: SignupForm): Result[Signup, FieldError] {
    if (form.email.len() == 0) {
        return Err(FieldError.Missing(field = "email"));
    }
    if (not form.agreed) {
        return Err(FieldError.NotAgreed);
    }
    let email = parseEmail(form.email).map_err(func(r: string): FieldError => FieldError.Invalid(field = "email", reason = r))?;
    let age = parseAge(form.age).map_err(func(r: string): FieldError => FieldError.Invalid(field = "age", reason = r))?;
    return Ok(Signup(email = email, age = age));
}

func message(err: FieldError): string {
    return match err {
        FieldError.Missing(field) => i"please fill in {$ field $}",
        FieldError.Invalid(field, reason) => i"{$ field $}: {$ reason $}",
        FieldError.NotAgreed => "please accept the terms",
    };
}

func createAccount(signup: Signup): int {
    if (signup.email.ends_with("@busy.example")) {
        throw HttpError(503);
    }
    return 1000 + signup.age;
}

func submit(form: SignupForm): string {
    match validate(form) {
        Err(err) => {
            return message(err);
        },
        Ok(signup) => {
            try {
                let id = createAccount(signup);
                return i"created account {$ id $}";
            } catch (HttpError as e) {
                return i"try again later ({$ e.message $})";
            }
        },
    }
}

func main(): void {
    println(submit(SignupForm(email = "", age = "30", agreed = true)));
    println(submit(SignupForm(email = "ada@example.com", age = "30", agreed = false)));
    println(submit(SignupForm(email = "ada@localhost", age = "30", agreed = true)));
    println(submit(SignupForm(email = "ada@example.com", age = "thirty", agreed = true)));
    println(submit(SignupForm(email = "lin@busy.example", age = "41", agreed = true)));
    println(submit(SignupForm(email = " Ada@Example.com", age = "36", agreed = true)));
}
```

*Listing 12-12: Expected failures as `Result`, infrastructure failures as exceptions*

```text
please fill in email
please accept the terms
email: 'localhost' is not a domain
age: 'thirty' is not a whole number
try again later (server answered 503)
created account 1036
```

`validate` reads top to bottom: each `?` either produces a clean value or ends the function with a `FieldError`. `message` maps every reason to text, and because `match` must be exhaustive, adding a new `FieldError` variant later forces you to write its message. The `try` in `submit` is the only place that knows about the server.

## Try it yourself

1. Add a `TooYoung(minimum: int)` variant to `FieldError`, return it from `validate` instead of the range error, and see which line the compiler sends you to.
2. Give `HttpError` a `retryable(): bool` method (true for 502, 503, and 504) and make `submit` print a different message for retryable failures.
3. Rewrite `Spinner` and `save` from Listing 12-10 so that `save` shows two indicators and hides both in one `finally`.

## Summary

- Throw with `throw SomeError("…")`; catch with `catch (Type as e)`, most specific first; `finally` always runs. User exceptions extend `Exception` (or a built-in such as `ValueError`) and pass the message with `super(message)`.
- An uncaught exception prints `Uncaught Type: message` with `at function (file:line:col)` frames and exits with status 1. `throw … from e` adds a `Caused by:` section.
- `panic` is not available in hosted or web code in Loup 1.25.1; throw instead. `IOError`, `FileNotFoundError`, and `ParseError` are not built in.
- `Result[T, E]` is for expected failures: `match` it, or use the snake_case helpers (`unwrap_or`, `map_err`, …). Printing an `Err` throws `RenderError`; use `repr` to inspect it.
- `?` returns an `Err` from a `Result`-returning function; convert error types with `map_err`. Use `attempt` to send an `Err` to a `catch Err(e)` arm instead.
- Known gaps in 1.25.1: integer overflow wraps, `to_int()` returns `0` on bad input, `?` on an `Option` yields `Err(None)`, `defer` never runs (use `finally`), and `async` bodies run eagerly, so make each call and its `await` inside the same `try`.
