---
slug: 92-errors
title: Errors & exit codes
description: How errors work in Synsema — raise, try/recover, the rich --explain report, error classification, and the exit codes used by run/test/check.
example_ids: [errors]
---

# Errors & exit codes

Press **Run** to see a caught error; edit it (remove the `try`/`recover`) to see an uncaught `Runtime error: …`.

```synsema
-- Doc example: how errors surface — raise, try/recover, and a clean message.
-- Edit this and press Run: remove the try/recover to see an uncaught
-- "Runtime error: x must be >= 0 …" instead.
intent: "doc example: errors"

task risky(x)
    when x < 0
        raise("x must be >= 0, got " + text(x))
    give x * 2

task fails()
    give risky(-1)

-- run shows the recovery in action:
try
    print("risky(5) = " + text(risky(5)))
    print("risky(-3) = " + text(risky(-3)))      -- this one raises
recover err
    print("caught → " + err)

test "raise fails; try/recover catches the message"
    assert_eq(risky(5), 10)
    let caught be ""
    try
        let bad be risky(-1)
    recover err
        set caught to err
    assert(contains(caught, "must be >= 0"))
    assert_error(fails)
```

## Raising & catching

```synsema
raise("message")            -- fail deliberately (or re-propagate inside recover)

try
    risky()
recover err
    log "failed: " + err    -- err is the message (text)
    raise(err)              -- RE-PROPAGATE; without it, recover swallows the error
```

`give` and `stop` are **not** errors — they pass through `try/recover`. (`fail(...)` builds an HTTP response, it does not raise.) Test that something raises with `assert_error(task)`. The statement form `raise "msg"` / `raise err` (no parens) works too, and a bare `raise` alone is a parse error with the fix in the message. ⚠️ On the published v0.5.1 and older, `raise "msg"` without parens was two inert expressions — a silent no-op; on those binaries always `raise("msg")`.

One more error worth recognizing on sight: `'decide' is a reserved word in Synsema; choose another name for the member after '.'` (on v0.5.1 and older: the cryptic `Expected IDENTIFIER, got DECIDE`) means a **hard keyword** was used as an export, parameter, or member name — `mod.decide(...)`, `task wait(reason)`. The LLM words `reason`/`decide`/`analyze`/`generate` are reserved everywhere; rename (`resolve`, `why`).

## Exit codes

| Exit | When |
|---|---|
| `0` | success |
| `1` | parse error, runtime error, or any spawned **agent** ended in `ERROR` |
| `2` | usage error — a missing argument, `synsema openapi` without `serve`, or (v0.6.14+) an **unknown `--flag`** (rejected, not ignored, on `run`/`test`/`conform`) |

Plain `run` prints a stable one-liner: `Runtime error: <file>:<line>:<col>: <msg>`. Measuring it in a shell? Redirect (`… >/dev/null; echo $?`) — don't pipe before `echo $?`, or you read the pipe's code.

## Rich diagnostics (`--explain`)

```sh
synsema run --explain program.syn                # human-readable, on stderr
synsema run --explain --format json program.syn  # structured, for tools/agents
```

The report adds: source context, call stack, visible variables, the program intent, a **classification** (`data` / `io` / `logic` / `capability` / `type`), whether it's recoverable, and fix suggestions. The exit code is unchanged either way.

## Common error shapes

- **Capability:** `Capability not granted: net("…")` — add the matching `require` (or widen the scope). If it says *declared but above the host ceiling*, the `require` is already there: only the host (`--sandbox`/`--cap-set`/the embedder's ceiling) can allow it. `Capability not granted: file_read("…")` from a `render` means a **disk** template read wasn't declared — add `require file.read("…")` (or bundle the template with `synsema build`).
- **Pure profile:** `<name>: not available in the pure profile — this run has no filesystem` (and likewise no child processes / sockets / database drivers / scheduler threads) — the builtin is walled off by `--profile pure`; drop the flag to run it natively. See [Sandboxing](22-sandbox).
- **`run_program`:** `above parent ceiling` / `above parent profile` in the child's audit (a child asked for more than the parent lends — trimmed, not fatal); `run_program: timed out after Ns` (`timed_out: true`); `run_program: max depth N exceeded`; `run_program: env "X" is a secret — reveal() it explicitly`.
- **`synsema build`:** `bundle corrupt (sha256 mismatch)` (a tampered built binary refuses to run); `"<path>" is part of the bundle (read-only)` (a write to a bundled asset); `this is a built program (synsema build); rebuild it…` (`--engine update` on a built binary); at build time, `a `use` with a dynamic path cannot be bundled` and `'…' escapes the bundle root`.
- **Type:** an operation got the wrong type (e.g. `as_secret(123)` → "expects text or bytes").
- **Data:** a missing map key, an out-of-range index, invalid JSON in `json_decode`.
