# Chapter 12: Error handling

Loup has two ways to report failure. An *exception* is thrown with `throw` and travels up the call stack until a `try`/`catch` handles it. A *`Result[T, E]`* is an ordinary return value that the caller inspects (chapter 8). This chapter covers both: the built-in exceptions, declaring your own, catching and re-throwing, chaining causes, `Result` with `?` and `attempt`, and what an uncaught exception looks like in a native binary.

## Throwing and catching

`throw` takes an exception object. `try` runs a block, and each `catch (Type as name)` clause handles exceptions of that type (or a subtype); the first matching clause wins. A user exception is a class that extends `Exception` and passes its message to `super`:

```loup
@mode(host.native)

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

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

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

    pub modifier func withdraw(amount: int): void {
        if (amount > this.balance) {
            throw InsufficientFundsError(amount, this.balance);
        }
        this.balance = this.balance - amount;
    }

    pub func remaining(): int { return this.balance; }
}

func main(): void {
    let wallet = Wallet(100);
    safe {
        try {
            wallet.withdraw(30);
            wallet.withdraw(500);
        } catch (InsufficientFundsError as e) {
            println(i"short by {$ e.requested - e.available $}: {$ e.message $}");
        }
    }
    println(wallet.remaining());
}
```

*Listing 12-1: A custom exception with extra fields*

```text
short by 430: requested 500, available 70
70
```

The first withdrawal succeeded and the second threw, so the balance is 70. Every exception has a `message` and an optional `cause` (below). One clause can catch several types, `catch (IndexError | KeyError as e)`, and `catch (Exception as e)` catches everything. Inside a handler, `throw e;` re-throws. A `finally` block runs on every path out of the `try` (Listing 5-9).

### The built-in exceptions

The runtime throws these, and you can throw and catch them too (Language Reference §1-18):

| Exception | Thrown by |
|---|---|
| `ValueError`, `TypeError` | invalid arguments; conventional for your own validation |
| `IndexError`, `KeyError` | `xs[i]` out of range, `d[k]` with a missing key |
| `IoError` | file and process operations (`readText`, `writeText`, …) |
| `NumericOverflowError`, `IntegerDivisionError` | checked arithmetic, division by zero (chapter 3) |
| `UnwrapError`, `RenderError` | `unwrap()` on `None`, printing an `Err` (chapter 8) |
| `TimeoutError` | operations with a deadline |
| `RuntimeError` | everything else the runtime detects |

All of them derive from `Exception`. Declare your own exception classes for your program's failures, as in Listing 12-1, rather than overloading the built-in ones.

### Mistake: the catch syntax from TypeScript or Kotlin

```loup,does_not_compile
@mode(host.native)

func main(): void {
    try {
        println(readText("/etc/app/missing.toml"));
    } catch (e: IoError) {
        println("missing");
    }
}
```

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

The type comes first and the name after `as`: `catch (IoError as e)`. Only the first message matters here; the rest follow from the parser having lost its place.

### Mistake: exception names from other languages

Python has `IOError` and Java has `FileNotFoundException`; Loup has neither. The I/O exception is `IoError`, and there is no separate "file not found" class:

```loup,does_not_compile
@mode(host.native)

func main(): void {
    try {
        println(readText("/etc/app/missing.toml"));
    } catch (IOError as e) {
        println("missing");
    }
}
```

```text
main.lphc:6:14: error[E0001]: undefined type 'IOError'
```

`FileNotFoundError`, `NetworkError` and `ParseError` are likewise undefined. Catch `IoError` and read its `message`, or declare a class such as `class ParseError : Exception { … }` for your own parser.

## Chaining exceptions

When you catch a low-level exception and throw a higher-level one, attach the original as the cause with `from`. The cause is kept, printed if the exception is never caught, and readable as `e.cause`:

```loup,throws
@mode(host.native)

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

func loadConfig(path: string): string {
    try {
        return readText(path);
    } catch (IoError as e) {
        throw ConfigError(i"cannot load {$ path $}") from e;
    }
}

func main(): void {
    println(loadConfig("/etc/app/missing.toml"));
}
```

```text
Uncaught ConfigError: cannot load /etc/app/missing.toml
  at loadConfig (main.lphc:11:15)
  at main (main.lphc:16:13)

Caused by:
Uncaught IoError: read_text failed on '/etc/app/missing.toml': No such file or directory (os error 2)
  at loadConfig (main.lphc:9:16)
  at main (main.lphc:16:13)
```

The process exits with status 1. A handler can walk the chain itself:

```loup
#~ @mode(host.native)
#~ class ConfigError : Exception {
#~     new(msg: string) { super(msg); }
#~ }
func isDigits(text: string): bool {
    if (text.length() == 0) {
        return false;
    }
    for (c in text.toCharList()) {
        if (c < "0" or c > "9") {
            return false;
        }
    }
    return true;
}

func loadPort(text: string): int {
    try {
        if (not isDigits(text)) {
            throw ValueError(i"not a number: {$ text $}");
        }
        return text.toInt();
    } catch (ValueError as e) {
        throw ConfigError("invalid port setting") from e;
    }
}

func main(): void {
    try {
        println(loadPort("8080"));
        println(loadPort("80a"));
    } catch (ConfigError as e) {
        println(i"config error: {$ e.message $}");
        match e.cause {
            Some(c) => println(i"  caused by: {$ c.message $}"),
            None => println("  no cause recorded"),
        }
    }
}
```

*Listing 12-2: Reading an exception's cause*

```text
8080
config error: invalid port setting
  caused by: not a number: 80a
```

### Mistake: trusting `toInt()`

Listing 12-2 checks the digits itself because `string.toInt()` does not report bad input in 1.25.1. It returns 0:

```loup,not_desired_behavior
@mode(host.native)

func main(): void {
    let port = "80a".toInt();
    println(i"listening on {$ port $}");
}
```

```text
listening on 0
```

Validate text before converting it, as `isDigits` does, whenever the input comes from a user or a file.

## Uncaught exceptions in a native binary

Under `loup run`, an uncaught exception prints `Uncaught`, the type and message, a stack trace, and any causes, as above. A native binary prints exactly one line on standard error, the class name and message, and exits with status 1:

```text
ConfigError: cannot load /etc/app/missing.toml
```

There is no stack trace in a native binary today. If you need one in production, catch exceptions at the top of `main`, log what you need (including `e.cause`), and return a non-zero status. When an exception unwinds through native frames, the reference counts held by those frames are released.

## `Result` for expected failures

An exception suits failures that the immediate caller cannot do much about and that should travel up to a handler. A `Result[T, E]` suits failures that are a normal outcome the caller must deal with, such as invalid input or a lookup that misses. The `?` operator keeps code that passes `Result`s along readable (Listing 8-5).

The two styles meet in `attempt`. Inside a `try` block, `attempt expr` unwraps an `Ok`, or jumps to a `catch Err(e)` clause with the error value (Language Reference §1-5.10):

```loup
#~ @mode(host.native)
func parsePort(text: string): Result[int, string] {
    let n = text.toInt();
    if (n <= 0 or n > 65535) {
        return Err(i"bad port: {$ text $}");
    }
    return Ok(n);
}

func main(): void {
    var port = 0;
    try {
        port = attempt parsePort("99999");
    } catch Err(e) {
        println(i"falling back: {$ e $}");
        port = 8080;
    }
    println(port);
}
```

*Listing 12-3: `attempt` with a `catch Err` clause*

```text
falling back: bad port: 99999
8080
```

`catch Err(e)` handles `Result` errors only; it is not an exception handler, which is why it also works in freestanding code where exceptions do not exist.

Some rules of thumb:

- Return a `Result` from functions whose failure is part of their job (parsing, validation, lookups), and let the caller decide.
- Throw for broken assumptions and for failures that should abort a whole operation; catch them at a boundary (a request handler, `main`) rather than around every call.
- When converting one into the other, keep the original information: `from e` for exceptions, a descriptive error value for `Result`s.
- Do not print a `Result` directly; `match` on it (chapter 8).

## Summary

- `throw X(...)` raises; `catch (Type as e)`, `catch (A | B as e)` and `catch (Exception as e)` handle; `finally` always runs; `throw e;` re-throws.
- User exceptions extend `Exception` and call `super(message)`. Built-ins include `ValueError`, `IndexError`, `KeyError`, `IoError` (not `IOError`), `NumericOverflowError` and `UnwrapError`; there is no `FileNotFoundError`.
- `throw New(...) from e` chains a cause, shown as `Caused by:` and readable as `e.cause`.
- `loup run` prints a stack trace for an uncaught exception; a native binary prints one `Class: message` line. Both exit with status 1.
- `string.toInt()` returns 0 for bad input in 1.25.1; validate first.
- `Result` is for expected failures; `attempt` hands an `Err` to a `catch Err(e)` clause.
- Chapter 13 covers modules and packages.
