# Error handling

`try` / `catch` / `finally`, `throw`, and the `@error` type.

## Usage

### Simple throw (key + message)

```ops
@release {
  try {
    bash.shellcheck()
  } catch error {
    log.error($error.message)
  } finally {
    git.config()
  }
}

@install-hooks needs [install.go-install.lefthook] {
  try {
    bash {
      lefthook install
    }
  } catch error {
    throw hook-install-failed `Issue installing project hooks: {$error.message}`
  }
}
```

### Rethrow

```ops
try {
  release()
} catch error {
  log.error($error.message)
  throw $error
}
```

Rethrowing preserves the original error and appends the current location to its stack.

### Structured throw (full @error construction)

```ops
try {
  infra.deploy(datacenter: $dc)
} catch error {
  throw @error {
    key: "deploy-failed"
    code: e001
    message: `deployment to {$dc} failed after {$attempt} attempts`
    help: "Check the datacenter's health endpoint and retry, or use --allow-remote to skip"
    context: {
      datacenter: $dc
      attempt: $attempt
      original: $error.key
    }
  }
}
```

### Matching on error keys

```ops
try {
  release()
  deploy-all()
} catch error {
  match $error.key {
    "version-exists" { log.warn($error.message) }
    "deploy-failed" { log.error($error.message) }
    * { throw $error }
  }
}
```

## Grammar

```
TryCatch        = 'try' '{' Statement* '}'
                  ('catch' IDENT? '{' Statement* '}')?
                  ('finally' '{' Statement* '}')?

ThrowStmt       = 'throw' ErrorKeyLit (STRING_LIT | TEMPLATE_LIT)
                | 'throw' VarRef
ThrowErrorStmt  = 'throw' '@error' '{' ErrorBody '}'
ErrorBody       = (ErrorField)*
ErrorField      = ('key' | 'code' | 'message' | 'help' | 'context') ':' Atom

ErrorKeyLit     = IDENT ('-' IDENT)*
```

The simple form takes two positional parts: `throw <key> <message>`. The structured form has named fields including the required `help`.

## Error object shape

Every error — whether thrown by the engine, an op, or DSL `throw` — conforms to the `@error` type:

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | `<c>###` | auto | Error code. `e` = user, `E` = engine. `###` is hex. |
| `key` | string | yes | Kebab-case identifier, <32 chars, no spaces. |
| `message` | string | yes | What went wrong. |
| `help` | string | structured | What to do about it (Rust/Elm philosophy). Required in `@error` blocks; absent on simple throws unless the engine fills it. |
| `origin` | string | auto | Where the error originated. Auto-populated by the engine; `custom` for all user errors. |
| `context` | object | no | Arbitrary key-value pairs for structured metadata. |
| `location` | location | auto | `file:line:col` — filled by the engine at throw site. |
| `stack` | array | auto | Structured error stack. Appended on rethrow. |

### Origin values

The `origin` field is auto-populated based on where the error surfaces. Users never set it — all user throws get `custom`.

| Origin | Meaning |
|--------|---------|
| `custom` | User-defined error (any `e`-prefixed throw) |
| `syntax` | `.ops` file syntax or semantic error |
| `lower` | AST → IR lowering error |
| `resolve` | Dependency graph, env merging, validation |
| `emit` | POSIX codegen error |
| `type` | Type validation error (built-in or custom) |
| `operation` | `.op.sh` file structural issue (definition, contracts) |
| `runtime` | Pipeline execution (emitted scripts) |

### Error code format: `<c>###`

Two prefixes. `e` for user errors, `E` for engine errors. Three hex digits (`000`–`FFF`) identify the specific error.

| Prefix | Owner |
|--------|-------|
| `e` | User / consumer (ops, pipelines, custom types) |
| `E` | Engine / shellops internals (compiler, resolver, emitter, runtime) |

`e000` is the generic user throw. `E` codes are assigned by the engine. If the error's origin (compiler vs runtime, `.ops` vs `.op.sh`, etc.) matters, that's metadata on the error object — not encoded in the code itself.

User error codes are optional. When a simple `throw key "message"` is used, the code defaults to `e000`. If a user assigns a code in `throw @error { ... }`, the engine validates the prefix is `e` and checks for collisions within project scope.

## IR representation

Canonical control-flow and value constraints live in [control-flow](../ir/control-flow.md), [values-and-types](../ir/values-and-types.md), and [sources-and-provenance](../ir/sources-and-provenance.md).

```json
// Simple throw
{ "role": "throw", "payload": [
  { "role": "key", "payload": "hook-install-failed" },
  { "role": "message", "payload": "Issue installing project hooks" }
]}

// Rethrow
{ "role": "throw", "payload": [{ "role": "var", "payload": "error" }] }

// Structured throw
{
  "role": "throw",
  "payload": [
    { "role": "key", "payload": "deploy-failed" },
    { "role": "code", "payload": "e001" },
    { "role": "message", "payload": "deployment failed" },
    { "role": "help", "payload": "Check the datacenter's health endpoint and retry" },
    { "role": "context", "payload": [
      { "role": "entry", "name": "datacenter", "payload": [{ "role": "var", "payload": "dc" }] },
      { "role": "entry", "name": "attempt", "payload": [{ "role": "var", "payload": "attempt" }] }
    ]}
  ]
}

// try/catch/finally
{
  "role": "branch",
  "payload": [
    { "role": "case", "payload": [
      { "role": "when", "payload": "always" },
      ...
    ]},
    { "role": "case", "payload": [
      { "role": "when", "payload": [{ "role": "failure", "payload": "previous" }] },
      { "role": "error", "payload": "error" },
      ...
    ]},
    { "role": "finally", "payload": [
      { "role": "when", "payload": "always" },
      ...
    ]}
  ]
}
```

## Runtime

- **`@throw`** — Signals a catchable error. Accepts three forms matching the DSL (simple, rethrow, structured). Caught by DSL-level `try` / `catch`. Distinct from `@fatal`.
- **`@fatal`** — Logs then hard-exits. Not catchable.
- **`@error`** — Built-in structural type. Validates error schema at construction time (code range, key constraints, required fields).

## Implementation

**Tier 9** (`if` / `try` / `with`): `ast.rs` — `Statement::TryCatch`, `Statement::Throw`, `Statement::ThrowError`. `parser.rs` — parse `try` / `catch` / `finally`, `throw` forms. `lower.rs` — emit nested IR.

**Tier 19** (emitter: control flow): Emit error metadata capture for `@throw` and make `@error` fields available in `catch` blocks via flattened shell variables.

## Design decisions

- **Three throw forms:** Simple (`throw key "message"`) for quick use. Rethrow (`throw $error`) for propagation with stack appending. Structured (`throw @error { ... }`) for full control with required `help`.
- **`@error` is a built-in type** — not user-definable. It has a fixed schema so the engine can validate at compile time and tooling can rely on the shape.
- **Error keys are required** on all throws. Every error site gets a greppable identifier. Keys must be kebab-case, <32 characters, no spaces.
- **Help is required on structured throws** (Rust/Elm philosophy). The `throw @error { ... }` form requires a `help` field — every structured error must answer "what do I do now?" Simple throws (`throw key "message"`) don't require it, keeping quick one-liners lightweight. Engine errors always include actionable help.
- **Two code prefixes:** `e` for user errors, `E` for engine errors. That's it. The `origin` field carries the detail about where the error originated — auto-populated, never set by users.
- **Hex codes are optional on user throws** — defaults to `e000`. Teams that want structured error catalogs can assign unique codes (`e001`–`eFFF`); the engine checks for collisions within project scope.
- **Non-zero exit** from inline `bash { ... }` generates an `@error` with key `bash-exit`, the exit code in context, and a help string pointing to `try`/`catch` wrapping. Wrap in `try` / `catch` when recovery is needed.
- **Catch variable** is optional. When present, it binds the `@error` object as a bare identifier (no `$` — binding position, not a dereference). Referenced as `$error` inside the catch body, with dotted access to fields: `$error.key`, `$error.message`, `$error.help`, `$error.code`, `$error.context`, `$error.stack`.
- **Rethrow** preserves the original error object and appends the rethrow location to the stack. This enables tracing error propagation through nested try/catch.

## Caveats

- Only `@throw` (and DSL-level errors surfaced as such) are catchable; `@fatal` and process death outside the DSL model are not recoverable via `catch`.
- User error code collisions are checked within project scope. Cross-project code collisions (e.g., two imported pipelines both using `e001`) are not checked — keys are the primary matching mechanism.

## Validation

Bats tests for `try` / `catch` / `finally` variants, all three throw forms, rethrow stack appending, `@error` type validation, user code range enforcement, key constraint checking.
