# EX Script — Error Handling Model

## The model in one paragraph

Expected failures are **values**, not exceptions. A function that can fail is
declared with `!` (`fun readFile(p: String) -> String!`) and returns a `Result`
with exactly two shapes: `Ok(value)` or `Err(error)`. The `?` operator propagates
failures up the stack in one token. `raise` creates a failure inside a `!`
function. The compiler makes silent failures impossible: a `Result` that is
dropped without being handled is a compile error. `try/catch` remains, but only
as the bridge to JavaScript code that throws.

## The three kinds of failure

| Kind | Example | Mechanism |
|---|---|---|
| Expected failure | file missing, bad input, timeout, network error | `Result` values, `!`, `?`, `raise` |
| Bug | logic error, invariant violated | `.require()`, `expect()` — crash loudly at the point of the lie |
| Foreign failure | JS library throws | `try/catch` at the boundary, converted to `Result` |

The language distinguishes "this can happen in normal operation" from "this
should never happen". Expected failures are handled; bugs are loud.

## Declaring fallible functions

```
fun readConfig(path: String) -> Config! {
    let text = std.io.readFile(path)?      // propagates any failure
    Config.parse(text)?                    // last expression; `?` propagates too
}
```

- The `!` suffix on the return type means: *may fail with an `Err`*.
- Inside a `!` function, `?` on a `Result` unwraps the value or returns the
  error early.
- `raise "message"` fails explicitly:

```
fun validateAge(a: Int) -> Int {
    if a < 0 { raise "age must be non-negative, got {a}" }
    a
}
```

`raise` compiles to a failure carrying the message and a stack trace.

## Handling failures

All of these are exhaustive — a `Result` cannot be silently dropped:

```
// 1. Propagate
fun outer() -> String! { readConfig(path)? }

// 2. Match — full control
match readConfig("config.json") {
    Ok(config) -> start(config)
    Err(e)     -> io.println("startup failed: {e}")
}

// 3. Fallback (lazy)
let config = readConfig("config.json") or Config.default()

// 4. Pattern-let
if let Ok(config) = readConfig("config.json") { start(config) }

// 5. Required (checked unwrap, crash with a good message)
let config = readConfig("config.json").require("config must exist")
```

`.require(msg)` is the "this failure is a bug" escape: it crashes with the
message and the underlying error's stack. It is used at trust boundaries, never
as a habit.

## Asynchronous failures

The same model applies to async functions:

```
async fun fetchUser(id: Int) -> User! {
    let res = await http.get("/users/{id}")?
    User.from(res.json())?
}
```

- `await` unwraps promises; `?` unwraps results — order: `await f()?` = await,
  then check failure.
- `all()` fails fast with the first error and cancels the others.
- `race()` completes with the first result, error included.
- `timeout(ms, fut)` turns a hang into an `Err` with a clear message.
- Unhandled-failure states are compile errors, not 3am production crashes.

## The `Err` value

```
Err("message")            // simple error
Err("msg", cause)         // chained error
e.message                 // human message
e.stack                   // full stack trace
e.cause                   // underlying error, if any
```

`Err` is a value, so it can be stored, logged, compared, and passed around like
any other value. Logging an `Err` prints the message and the chain of causes.

## The JavaScript boundary

```
try {
    let raw = legacyJsLibrary.doSomething()       // throws on failure
    let value: String = raw.require("expected a string")
    Ok(value)                                      // convert to Result
} catch e {
    Err("legacy library failed: {e}")
}
```

`try/catch` is the only place exceptions exist. Everything inside the language
uses values. When calling JS, the boundary function converts throws into
`Result`s so the rest of the program stays exception-free.

## Error messages

Errors in EX Script are designed to be *the* product. Compiler diagnostics
follow the same four-part structure:

```
error[E1007]: `username` may be missing here
  ┌─ src/main.xan:12:14
  │
12 │     greet(username)
  │          ^^^^^^^^ `username` has type `String?`
  │
  You're passing `username` to `greet`, which requires a guaranteed `String`.

  Possible fixes:
  1. Check that it exists first:
       if let name = username { greet(name) }
  2. Provide a fallback:
       greet(username or "Guest")
  3. Change `greet` to accept an optional value:
       fun greet(name: String?)
```

Runtime errors follow the same style — `Err` messages always include the chain
of causes and the originating location.

## Rules the compiler enforces

1. A `Result` value cannot be dropped unhandled.
2. `?` only inside a `!` function (or a `try` block, where it converts to an
   exception on the JS side).
3. `raise` only inside a `!` function.
4. `.require()` always receives a message (so the failure is explainable).
5. `try` without `catch` is an error (no silent exception swallowing).
6. `catch` without binding the error is an error.
7. `match` on a `Result` must cover `Ok` and `Err`.