# Chapter 10: Error Handling

**What you'll learn:** the three ways a Loup script deals with failure — `Option[T]` when a value may be absent, `Result[T, E]` when an operation can fail in an expected way, and exceptions for failures that should unwind the stack — plus the `?` and `attempt` operators that pass a `Result`'s error along without a `match` at every step.

Files go missing, input is malformed, a lookup finds nothing. Loup gives you a tool for each situation, and choosing the right one is most of the skill:

| Situation | Tool |
|---|---|
| A value might not be there (a lookup, an optional setting) | `Option[T]`: `Some(value)` or `None` |
| An operation can fail and the caller should decide what to do (parsing, validation) | `Result[T, E]`: `Ok(value)` or `Err(error)` |
| Something went wrong that the code at hand cannot handle (a missing file, a bug) | an exception: `throw`, `try`/`catch`/`finally` |

`Option`, `Result`, `Some`, `None`, `Ok` and `Err` are built in; you do not import them.

## `Option[T]`: Values That Might Be Absent

A function that may not find what it looks for returns an `Option`:

```loup
@mode(host.scripted)

struct User {
    id: int;
    name: string;
}

let users = [
    User(id = 1, name = "Ada"),
    User(id = 2, name = "Grace"),
];

func findUser(id: int): Option[User] {
    for (user in users) {
        if (user.id == id) {
            return Some(user);
        }
    }
    return None;
}

# 1. match on the two cases
let greeting = match findUser(2) {
    Some(u) => i"Hello, {$ u.name $}",
    None => "Hello, stranger",
};
println(greeting);

# 2. ?. reaches into an Option; the result is still an Option
println(repr(findUser(1)?.name));

# 3. ?? supplies a default when the value is None
println(findUser(7)?.name ?? "nobody");

# 4. unwrap() when absence would be a bug
println(findUser(1).unwrap().name);
println(findUser(9).is_none());
```

*Listing 10-1: Four ways to use an `Option`*

```text
Hello, Grace
Some("Ada")
nobody
Ada
true
```

- `match` forces you to say what happens in both cases.
- `x?.field` is `None` when `x` is `None` and `Some(x.field)` otherwise.
- `x ?? fallback` unwraps the `Option`, or evaluates `fallback` when it is `None`.
- `unwrap()` returns the value or throws `UnwrapError`; `expect("message")` does the same with your message. Use them only where `None` would mean a bug.

`T?` is shorthand for `Option[T]`: a field declared `email: string?` holds an `Option[string]`.

### Mistake: the spec's camelCase method names

The Language Reference writes the `Option` and `Result` methods in camelCase (`isSome`, `unwrapOr`). The 1.25 toolchain implements them in snake_case: `is_some`, `is_none`, `unwrap_or`, `is_ok`, `is_err`, `unwrap_err`, `map_err`. On an `Option`, a camelCase name is a compile error:

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

let port: Option[int] = Some(8080);
println(port.isSome());
```

```text
main.lpscr:4:9: error[E0001]: type 'Option[int]' has no method 'isSome'
```

The fix is `port.is_some()`. On a `Result` the mistake is worse, as the next section shows.

## `Result[T, E]`: Operations That Can Fail

A function whose failure is part of its normal behaviour returns a `Result`. The error type `E` can be anything: a `string`, a struct, a class.

```loup
@mode(host.scripted)

from system.text import (parseInt);

## A TCP port number from text, or a description of what is wrong with it.
func parsePort(text: string): Result[int, string] {
    let n = parseInt(text)?;
    if (n < 1 or n > 65535) {
        return Err(i"port out of range: {$ n $}");
    }
    return Ok(n);
}

for (input in ["8080", "99999", "http"]) {
    match parsePort(input) {
        Ok(port) => println(i"{$ input $}: port {$ port $}"),
        Err(reason) => println(i"{$ input $}: {$ reason $}"),
    }
}

println(parsePort("443").is_ok());
println(parsePort("http").unwrap_or(80));
```

*Listing 10-2: Returning and matching a `Result`*

```text
8080: port 8080
99999: port out of range: 99999
http: invalid integer: http
true
80
```

`parseInt` from `system.text` returns `Result[int, string]`. The `?` after it is explained in the next section; for now, read it as "if this is an `Err`, return that error from `parsePort`".

### Mistake: `unwrapOr` on a `Result`

On a `Result`, the camelCase spelling is *not* caught by the compiler. The script type-checks, runs, and fails when it reaches the call:

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

from system.text import (parseInt);

let port = parseInt("http").unwrapOr(80);
println(port);
```

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

The fix is the snake_case name, `parseInt("http").unwrap_or(80)`. When you see "method not found" on `Result` at run time, check the spelling first.

### Mistake: converting text with `toInt()`

`string` has a `toInt()` method, and it is tempting to use it for input:

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

let lines = ["42", "4x2", ""];
var total = 0;
for (line in lines) {
    total = total + line.toInt();
}
println(total);
```

```text
42
```

`toInt()` returns `0` for text that is not a number, so two bad lines were silently counted as zeros. For input you do not control, use `parseInt` (or `parseFloat`) from `system.text`, which returns a `Result` you must handle:

```loup
@mode(host.scripted)

from system.text import (parseInt);

let lines = ["42", "4x2", ""];
var total = 0;
for (line in lines) {
    match parseInt(line) {
        Ok(n) => total = total + n,
        Err(reason) => println(i"skipping: {$ reason $}"),
    }
}
println(total);
```

```text
skipping: invalid integer: 4x2
skipping: invalid integer: 
42
```

## The `?` Operator

Inside a function that returns a `Result`, `expr?` unwraps an `Ok` and, on an `Err`, returns that error from the function immediately. A chain of fallible steps reads like straight-line code:

```loup
@mode(host.scripted)

from system.text import (parseInt);

func parsePort(text: string): Result[int, string] {
    let n = parseInt(text)?;
    if (n < 1 or n > 65535) {
        return Err(i"port out of range: {$ n $}");
    }
    return Ok(n);
}

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

println(repr(parseAddress("localhost:8080")));
println(repr(parseAddress("localhost:99999")));
println(repr(parseAddress("localhost")));
```

*Listing 10-3: `?` passes each step's error to the caller*

```text
Ok(("localhost", 8080))
Err("port out of range: 99999")
Err("expected host:port, got 'localhost'")
```

### Mistake: `?` in a function that does not return `Result`

`?` needs somewhere to send the error, so it only works inside a function whose return type is a `Result`:

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

from system.text import (parseInt);

func doubled(text: string): int {
    let n = parseInt(text)?;
    return n * 2;
}

println(doubled("21"));
```

```text
main.lpscr:6:13: error[E_PROPAGATE_WRONG_RETURN]: E_PROPAGATE_WRONG_RETURN: the '?' operator can only be used inside a function returning Result (§1-5.7)
```

The same error appears if you use `?` at the top level of a script. Either make the function return a `Result` (`func doubled(text: string): Result[int, string]` and `return Ok(n * 2);`), or handle the error where it occurs with `match` or `unwrap_or`.

### Mistake: `?` on an `Option`

The Language Reference says `?` applies only to a `Result` and rejects it on anything else (`E_PROPAGATE_NOT_RESULT`, §1-5.7). The 1.25 compiler rejects it on an `int` or a `string`, but not on an `Option`, and what it does then is wrong:

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

func portSetting(config: dict[string, string]): Result[string, string] {
    let raw = config.get("port")?;
    return Ok(raw);
}

let empty: dict[string, string] = {};
println(repr(portSetting(empty)));
```

```text
Err(None)
```

`config.get` returns an `Option`. The `None` was turned into an `Err` carrying `None` — not the `string` the signature promises, and not a message anyone can act on. Convert the `Option` yourself, with `??` or `match`:

```loup
@mode(host.scripted)

func portSetting(config: dict[string, string]): Result[string, string] {
    let raw = match config.get("port") {
        Some(value) => value,
        None => {
            return Err("missing setting 'port'");
        },
    };
    return Ok(raw);
}

let empty: dict[string, string] = {};
println(repr(portSetting(empty)));
println(repr(portSetting({"port": "8080"})));
```

```text
Err("missing setting 'port'")
Ok("8080")
```

### Converting Error Types with `From[E]`

When the step's error type differs from the function's, `?` converts it with the function's error type's `from` method. The error type declares the conversion by implementing `From[E]`:

```loup
@mode(host.scripted)

struct ParseError {
    line: int;
    reason: string;
}

class AppError implements From[ParseError] {
    pub let message: string;

    new(message: string) {
        this.message = message;
    }

    pub static func from(err: ParseError): AppError {
        return AppError(i"line {$ err.line $}: {$ err.reason $}");
    }
}

func parseCount(line: int, text: string): Result[int, ParseError] {
    let n = text.toInt();
    if (n <= 0) {
        return Err(ParseError(line = line, reason = i"not a positive count: '{$ text $}'"));
    }
    return Ok(n);
}

func total(lines: list[string]): Result[int, AppError] {
    var sum = 0;
    var lineNo = 1;
    for (text in lines) {
        sum = sum + parseCount(lineNo, text)?;
        lineNo = lineNo + 1;
    }
    return Ok(sum);
}

for (input in [["3", "4", "5"], ["3", "four", "5"]]) {
    match total(input) {
        Ok(n) => println(i"total {$ n $}"),
        Err(e) => println(i"error: {$ e.message $}"),
    }
}
```

*Listing 10-4: `?` converts a `ParseError` into an `AppError`*

```text
total 12
error: line 2: not a positive count: 'four'
```

(Here `toInt()`'s zero for bad text is caught by the `n <= 0` check, so it is safe.) Leave out `implements From[ParseError]` and the compiler says exactly what is missing:

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

struct ParseError {
    line: int;
    reason: string;
}

class AppError {
    pub let message: string;

    new(message: string) {
        this.message = message;
    }
}

func parseCount(line: int, text: string): Result[int, ParseError] {
    return Ok(text.toInt());
}

func total(lines: list[string]): Result[int, AppError] {
    var sum = 0;
    for (text in lines) {
        sum = sum + parseCount(1, text)?;
    }
    return Ok(sum);
}

println(repr(total(["1"])));
```

```text
main.lpscr:23:21: error[E_PROPAGATE_MISSING_FROM]: E_PROPAGATE_MISSING_FROM: '?' propagates an error of type 'ParseError' out of a function whose error type is 'AppError', which does not implement `From[ParseError]` (§1-5.7)
```

## `attempt`: Handling Several `Result`s in One Place

`?` sends an error out of the function. Sometimes you want to handle it right here instead, without a `match` around every call. `attempt expr` unwraps an `Ok`; on an `Err` it jumps to the `catch Err(e)` arm of the enclosing `try` (§1-5.10):

```loup
@mode(host.scripted)

from system.text import (parseInt);

func parsePort(text: string): Result[int, string] {
    let n = parseInt(text)?;
    if (n < 1 or n > 65535) {
        return Err(i"port out of range: {$ n $}");
    }
    return Ok(n);
}

## Both ports, or the defaults if either is bad.
func portsOrDefault(http: string, https: string): (int, int) {
    try {
        let first = attempt parsePort(http);
        let second = attempt parsePort(https);
        return (first, second);
    } catch Err(e) {
        println(i"using defaults: {$ e $}");
        return (8080, 8443);
    }
}

println(portsOrDefault("80", "443"));
println(portsOrDefault("80", "https"));
```

*Listing 10-5: `attempt` sends every failure to one handler*

```text
(80, 443)
using defaults: invalid integer: https
(8080, 8443)
```

`catch Err(e)` is not an exception handler: it only receives errors from `attempt`. A `try` may have one `catch Err` arm alongside ordinary `catch` clauses and a `finally`.

`attempt` needs that arm to exist:

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

from system.text import (parseInt);

let port = attempt parseInt("80");
println(port);
```

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

## Exceptions

Exceptions are for failures that the code where they happen cannot sensibly handle: the file is not there, the data is corrupt, an index is out of range. They unwind the call stack until a `catch` handles them, so the functions in between do not mention them in their signatures.

### Catching an Exception

The file functions throw `IoError` when the operating system refuses:

```loup
@mode(host.scripted)

func loadSettings(path: string): string {
    return readText(path);
}

try {
    let text = loadSettings("missing-settings.toml");
    println(text);
} catch (IoError as e) {
    println(i"could not read settings: {$ e.message $}");
    println(i"error number: {$ e.code $}");
} finally {
    println("done");
}
```

*Listing 10-6: `try`, `catch` and `finally`*

```text
could not read settings: read_text failed on 'missing-settings.toml': No such file or directory (os error 2)
error number: 2
done
```

- `catch (Type as name)` handles exceptions of that type and its subclasses, and binds the exception to `name`.
- Every exception has a `message`. `IoError` also has `code`, the operating system's error number (`2` is "no such file").
- `finally` runs whether the `try` block finished, threw, or returned. A `return`, `break` or `continue` inside `finally` is a compile error (`E_FINALLY_CONTROL`).
- `catch` clauses are tried top to bottom, so put specific types first and `catch (Exception as e)` last. A bare `catch { … }` handles any `Exception`.

### Mistake: `catch (e: Type)`

TypeScript, Kotlin and Swift habits produce this:

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

try {
    let text = readText("settings.toml");
    println(text);
} catch (e: IoError) {
    println(e.message);
}
```

```text
main.lpscr:6:11: error[E0001]: expected RParen, found Colon
main.lpscr:6:11: error[E0002]: expected `{`
main.lpscr:6:11: error[E0001]: expected expression
main.lpscr:6:13: error[E0003]: expected `;`
main.lpscr:8:1: error[E0001]: expected expression
```

Only the first line matters; the rest follow from it. In Loup the type comes first and the name after `as`: `catch (IoError as e)`.

### Mistake: an exception class that does not exist

Python and Java names do not carry over:

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

try {
    let text = readText("settings.toml");
    println(text);
} catch (FileNotFoundError as e) {
    println("no settings file");
}
```

```text
main.lpscr:6:10: error[E0001]: undefined type 'FileNotFoundError'
```

`IOError` (capital O) gives the same error. A missing file is an `IoError` whose `code` is `2`:

```loup
@mode(host.scripted)

try {
    let text = readText("settings.toml");
    println(text);
} catch (IoError as e) {
    if (e.code == 2) {
        println("no settings file");
    } else {
        println(i"cannot read settings: {$ e.message $}");
    }
}
```

```text
no settings file
```

The built-in exception classes you will meet most are:

| Class | Thrown when |
|---|---|
| `IoError` | a file, process or socket operation fails (has `code`) |
| `ValueError` | a value is unacceptable for the operation |
| `TypeError` | a value has the wrong type at run time |
| `IndexError` | a list index is out of range |
| `KeyError` | `d[key]` names a key that is not in the dict |
| `IntegerDivisionError` | an integer is divided by zero |
| `UnwrapError` | `unwrap()` or `expect()` meets `None` or `Err` |
| `RuntimeError` | a general failure with no more specific class |
| `TimeoutError` | an operation ran out of time |

All of them extend `Exception`. There is no built-in `FileNotFoundError`, `NetworkError` or `ParseError`; declare your own classes for those (next section). You can throw any of the classes above except `IoError`, which only the runtime raises.

### Several Types in One Clause

```loup
@mode(host.scripted)

let stock = {"bolt": 100, "nut": 250};
let bins = ["bolt", "nut"];

for (i in [0, 5]) {
    try {
        let name = bins[i];
        println(i"{$ name $}: {$ stock[name] $}");
    } catch (IndexError | KeyError as e) {
        println(i"lookup failed: {$ e.message $}");
    }
}
```

```text
bolt: 100
lookup failed: list index 5 out of range (len 2)
```

### Throwing and Defining Exceptions

`throw` takes an exception object. Your own exception classes extend `Exception`, can add fields, and pass the message to `super`:

```loup
@mode(host.scripted)

class ValidationError : Exception {
    pub let field: string;

    new(field: string, reason: string) {
        super(i"{$ field $}: {$ reason $}");
        this.field = field;
    }
}

func checkAge(age: int): int {
    if (age < 0) {
        throw ValidationError("age", "must not be negative");
    }
    if (age > 150) {
        throw ValidationError("age", "must be at most 150");
    }
    return age;
}

for (age in [42, -1, 200]) {
    try {
        println(i"accepted {$ checkAge(age) $}");
    } catch (ValidationError as e) {
        println(i"rejected {$ e.field $} ({$ e.message $})");
    }
}
```

*Listing 10-7: A custom exception class*

```text
accepted 42
rejected age (age: must not be negative)
rejected age (age: must be at most 150)
```

A class that declares no constructor gets the built-in one, so `class ConfigError : Exception {}` followed by `throw ConfigError("…")` is enough for a simple error.

Read an exception's `message` field rather than interpolating the exception itself: `i"{$ e $}"` prints something like `IndexError()`, not the message.

### Mistake: throwing a string

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

func checkAge(age: int): int {
    if (age < 0) {
        throw "age must not be negative";
    }
    return age;
}

println(checkAge(-1));
```

```text
main.lpscr:5:9: error[E_THROW_NON_ERROR]: cannot throw value of type 'string': thrown values must extend Error
```

Only exception objects can be thrown. Wrap the text in an exception class: `throw ValueError("age must not be negative");`. (The Language Reference calls this error `E_THROW_NOT_EXCEPTION` and the base class `Exception`; the 1.25 message still says `Error`.)

### Uncaught Exceptions

An exception that nothing catches ends the script. `loup run` prints `Uncaught`, the class, the message and the call stack, and exits with status 1:

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

class ValidationError : Exception {}

func checkAge(age: int): int {
    if (age < 0) {
        throw ValidationError("age must not be negative");
    }
    return age;
}

println(checkAge(30));
println(checkAge(-1));
println("never printed");
```

```text
30
Uncaught ValidationError: age must not be negative
  at checkAge (main.lpscr:7:15)
  at <main> (main.lpscr:13:9)
```

The innermost frame is listed first: the exception was created in `checkAge` at line 7 and the call came from line 13 of the script.

### Chaining: `throw … from`

When you catch a low-level exception and throw a more meaningful one, keep the original as the `cause`:

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

class ConfigError : Exception {}

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

try {
    loadConfig("app.toml");
} catch (ConfigError as e) {
    println(i"handled: {$ e.message $}");
}

loadConfig("app.toml");
```

*Listing 10-8: Keeping the original exception as the cause*

```text
handled: cannot load config 'app.toml'
Uncaught ConfigError: cannot load config 'app.toml'
  at loadConfig (main.lpscr:9:15)
  at <main> (main.lpscr:19:1)

Caused by:
Uncaught IoError: read_text failed on 'app.toml': No such file or directory (os error 2)
  at loadConfig (main.lpscr:7:16)
  at <main> (main.lpscr:19:1)
```

The cause is also available in code as `e.cause`, an `Option`.

> ⚠ **Planned.** The Language Reference (§1-6.10) says that an exception thrown inside a `catch` block without `from` automatically gets the caught exception as its cause. The 1.25 toolchain does not do this — `e.cause` is `None` — so write `from e` whenever you want the chain.

### Mistake: `panic`

Many languages have a `panic` or `abort` for "this cannot happen". The Language Reference lists `panic(msg)` in every mode, but in Loup 1.25 it is not available in hosted code:

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

func checkedRatio(part: int, whole: int): int {
    if (whole == 0) {
        panic("whole must not be zero");
    }
    return part * 100 / whole;
}

println(checkedRatio(1, 4));
```

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

Throw an exception instead — `throw RuntimeError("whole must not be zero");` — which a caller can catch or which ends the script with a trace. For a point that really cannot be reached, the `unreachable` expression stops the program with `Uncaught RuntimeError: panic: reached an 'unreachable' expression`; it cannot be caught.

## Cleaning Up

`finally` (Listing 10-6) runs cleanup on every path out of a `try`. For a resource object, `with (name = expr) { … }` calls the object's `close()` when the block ends, even when it ends with an exception, provided its class implements `Closeable`; Chapter 4 introduces it and Chapter 19 covers it in depth.

> ⚠ **Planned.** `defer action;` (§1-6.5) is meant to run `action` when the enclosing block exits. The 1.25 toolchain accepts `defer` but `loup run` never executes the deferred action. Until it does, use `try`/`finally` or `with` for cleanup.

## Choosing the Right Mechanism

| Situation | Use |
|---|---|
| The value may simply not exist | return `Option[T]` |
| Input may be malformed; the caller decides what to do | return `Result[T, E]`, pass it along with `?` |
| Several fallible steps, one fallback | `attempt` inside `try … catch Err(e)` |
| The operating system or the environment fails | let the `IoError` propagate; catch it where you can report it |
| A caller broke a rule (a bad argument) | `throw ValueError(…)` or your own exception class |
| "This cannot happen" | `throw RuntimeError(…)`, or `unreachable` |

## A Worked Example: Reading an Inventory File

This script parses lines of `name, quantity, price`, skipping blank lines and comments. Each line is parsed with `?`; the line number is attached where the lines are counted; the top level decides what to print.

```loup
@mode(host.scripted)

from system.text import (parseInt, parseFloat);

struct Item {
    name: string;
    quantity: int;
    price: float;
}

struct LineError {
    line: int;
    reason: string;
}

## One `name, quantity, price` line.
func parseLine(text: string): Result[Item, string] {
    let fields = text.split(",");
    if (fields.length() != 3) {
        return Err(i"expected 3 fields, got {$ fields.length() $}");
    }
    let quantity = parseInt(fields[1].trim())?;
    let price = parseFloat(fields[2].trim())?;
    return Ok(Item(name = fields[0].trim(), quantity = quantity, price = price));
}

## Every item, or the first line that is wrong.
func parseInventory(text: string): Result[list[Item], LineError] {
    var items: list[Item] = [];
    var lineNo = 0;
    for (line in text.split("\n")) {
        lineNo = lineNo + 1;
        let trimmed = line.trim();
        if (trimmed.length() == 0 or trimmed.startsWith("#")) {
            continue;
        }
        match parseLine(trimmed) {
            Ok(item) => items.push(item),
            Err(reason) => {
                return Err(LineError(line = lineNo, reason = reason));
            },
        }
    }
    return Ok(items);
}

func report(text: string): void {
    match parseInventory(text) {
        Ok(items) => {
            var value = 0.0;
            for (item in items) {
                value = value + item.price * (item.quantity as float);
            }
            println(i"{$ items.length() $} items, stock value {$ value $}");
        },
        Err(e) => println(i"line {$ e.line $}: {$ e.reason $}"),
    }
}

report("""
# name, quantity, unit price
bolt, 100, 0.25
nut, 250, 0.12
washer, 80, 0.05
""");

report("""
bolt, 100, 0.25
nut, lots, 0.12
""");

report("bolt, 100");
```

*Listing 10-9: Result, `?` and context in one small program*

```text
3 items, stock value 59
line 2: invalid integer: lots
line 1: expected 3 fields, got 2
```

In a real script the text would come from `readText(path)`, and the `IoError` for a missing file would be left to propagate to the top, where the uncaught-exception report says exactly what went wrong. (The value `59.0` prints as `59`: Loup prints a whole-number `float` without a decimal point.)

## Summary

- `Option[T]` (`Some`/`None`) is for absence; use `match`, `?.`, `??`, and `unwrap()` only where absence is a bug.
- `Result[T, E]` (`Ok`/`Err`) is for expected failure. Its methods are snake_case in 1.25: `is_ok`, `unwrap_or`, `map_err`. A camelCase name on a `Result` compiles and fails at run time.
- `expr?` returns an `Err` from the enclosing `Result`-returning function, converting it through `From[E]` when the types differ. Do not use `?` on an `Option`: 1.25 compiles it and produces `Err(None)`.
- `attempt expr` sends an `Err` to the enclosing `try`'s `catch Err(e)` arm.
- Exceptions: `throw`, `catch (Type as e)`, `catch (A | B as e)`, `finally`, `throw … from e`. Custom exceptions extend `Exception`. `IoError` (with `code`) is the file-system exception; `FileNotFoundError` and `IOError` do not exist.
- An uncaught exception prints `Uncaught Class: message` and a stack trace and exits with status 1.
- `panic` is not available in hosted code; throw instead. `defer` does not run yet; use `finally` or `with`.

## Try It Yourself

1. Write `parseDuration(text: string): Result[int, string]` that accepts `"90s"`, `"5m"` and `"2h"` and returns seconds. Use `parseInt` and `?`, and test it on good and bad input.
2. Write a script that reads a file named on the command line (`args()[0]`) and prints its line count. Catch `IoError` and print a one-line message instead of the trace when the file is missing.
3. Rewrite Listing 10-9 so `parseInventory` returns `Result[list[Item], AppError]` and `AppError` implements `From[LineError]`. Which `match` can you replace with `?`?
4. Write `throw "oops";` and then `catch (e: Exception)`, and read both diagnostics.
