Error Handling
Sema has no unwinding exceptions and no raise. Every failure is a typed
value that flows through the program’s types. You propagate it with ?, handle
it with flat expect … / except E as e: arms, and transform it with prelude
combinators. Because failures are values, the effect system sees them, the debugger
can replay them, and blame is preserved end to end.
Typed failures
Section titled “Typed failures”Every failure the language names is a struct conforming to the prelude Error
trait (carrying a blame label, source span, evidence, and journal ref). The ones
you will meet most:
| Error | Raised by |
|---|---|
ContractViolation |
a failed require/ensure/invariant |
SemanticsViolation |
a failed semantics(...) guard |
OverflowError |
checked integer overflow |
DivisionByZero |
division/modulo by zero |
ValueError |
math-domain errors, bad conversions |
BudgetExceeded |
a with budget(...) cap being crossed |
DecodeError |
a failed typed decode / schema parse |
ForeignError |
a failure crossing an FFI / port boundary |
SimulationFailed |
a simulate body that could not satisfy its contract |
A fallible expression types as Result[T, E] (or the sum T | E₁ | E₂ that
expect scrutinizes). There is no null; absence is Option[T]. See
Types.
? — propagate the failure upward
Section titled “? — propagate the failure upward”? unwraps the success value, or returns the failure from the enclosing
function — which must declare a compatible failure type. Propagation is blame-
and trust-preserving: a forwarded error keeps its original blame party and the
carried value’s trust labels, so escalation cannot launder either.
def import_statement(path: Path) -> Result[Statement, IngestError] !{fs.read}: raw = fs.read_text(path)? # ? propagates the typed failure upward stmt = parse_statement(raw)? return Ok(stmt)expect / except — flat, ordered handling
Section titled “expect / except — flat, ordered handling”expect <expr>: scrutinizes a fallible expression; except E as e: arms handle
each typed failure. The arms are siblings, ordered, and exhaustiveness-checkable
— there is no nesting and no unwinding:
expect rows = load_rows(path): reconcile(rows)except ContractViolation as v: quarantine(path, evidence=v)except ForeignError as e: escalate(e)The handler value carries full evidence — for a SemanticsViolation, that means
v.predicate, v.judge, v.score, v.threshold, v.excerpts, and v.blame.
The expect semantics(...): / except SemanticsViolation as v: block (see
Verification) is this exact construct applied to a
semantic predicate — the guarded expression types T | SemanticsViolation and the
except arm is the handling branch.
unwrap and expect-message aborts
Section titled “unwrap and expect-message aborts”unwrap() is a checked-region abort with blame — it converts a failure into a
replayable UnwrapFailed fault. It is for cases you have already proven cannot
fail, or for scripts. @assure(gold) functions reject reachable unwrap the
way release builds reject a reachable todo:
value = maybe_value.unwrap() # aborts with blame if it's None/ErrContracts as failures
Section titled “Contracts as failures”Contract clauses raise typed ContractViolation values on failure — and crucially,
the raw result cannot flow onward (this is the core Sema principle: a value that
fails its contract is typed as failed):
def normalize(scores: list[f32]) -> list[f32]: require len(scores) > 0 ensure all(0.0 <= s <= 1.0 for s in result) # a sound, fatal check ...
struct Account: balance: Money sem "Current settled account balance" invariant balance.minor_units >= 0requireis a boundary precondition.ensureis a postcondition (resultnames the return value) — and also works mid-body as a checked assertion over locals.invariantguards a struct across every mutation and boundary crossing.
The hard semantic assertion ensure semantics("...", x, alpha=0.02) (or any
calibrated coercion in ensure position) is Sema’s semantic assert — legal only
under a calibrated judge, failing as a ContractViolation carrying the judge’s
evidence. The soft form, statement-position check semantics(...), never blocks
— its Sim evidence is journaled. Full contract semantics live at
/neurosymbolic/contracts/.
Combinators — fallbacks without new syntax
Section titled “Combinators — fallbacks without new syntax”Expression-level recovery uses prelude methods on Result/Option:
| Combinator | Effect |
|---|---|
.or(default) |
substitute a fallback value (discarded failure is journaled as handled-by-default) |
.or_else(f) |
substitute via a fallback function |
.map_err(f) |
convert error types at a membrane so ? can propagate through a differently-typed caller |
.context("...") |
append a human-meaningful frame to the propagation trace before ? |
def snapshot(path: Path) -> Result[Report, ReportError] !{fs.read, model.embed}: raw = fs.read_text(path).context("loading ledger snapshot")? ledger = parse[Ledger](raw).map_err(ReportError.malformed)? fx = fetch_rates().or(cached_rates()) # fallback; discard journaled return Ok(render(ledger, fx))No cascade tax — the mainstream vocabulary, flat
Section titled “No cascade tax — the mainstream vocabulary, flat”Everything the try/catch world does maps 1:1 onto constructs that stay flat:
| Mainstream | Sema | Why it stays flat |
|---|---|---|
try |
expect <expr>: |
one block, many typed arms |
catch E |
except E as e: |
arms are ordered siblings, exhaustiveness-checkable |
| rethrow / delegate up | ? (+ .context("...")) |
one character; blame, trust, and origin ride along |
finally |
with <resource> as x: |
release runs on success, failure, and cancellation |
| retry / repair | supervise/heal, decode-repair |
recovery is scope- or boundary-owned |
| hand off to another party | emit FailureEvent(...) |
delegation is an event with origin intact |
An expect nested inside another expect arm more than two levels deep is a
style lint pointing you at ?/map_err — the cascade shape is treated as a
smell by the toolchain, not just by convention.
Failure modes
Section titled “Failure modes”?in a function whose failure type can’t carry the error → compile error listing the missing variant (fix with.map_err).exceptarm order shadowing a later arm → compile warning.- Catch and discard without journaling (
except E: pass) → the catch-and-swallow lint. Use.or(...)if defaulting is intended. - Reachable
unwrapunder@assure(gold)→ rejected.
What Sema deliberately does not have
Section titled “What Sema deliberately does not have”try/raise/finallyunwinding — invisible to effect rows, hostile to replay and blame.- Go-style
(value, err)tuples — handling is unenforced. - Silent
Option-ization of failures — evidence loss.
See also
Section titled “See also”- Types —
Option[T]andResult[T, E]. - Pattern Matching — matching
Ok/Err,Some/None. - Contracts —
require/ensure/checkin depth. - Control Flow —
withas the deterministicfinally.