Skip to content

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.

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.

? 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() 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/Err

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 >= 0
  • require is a boundary precondition.
  • ensure is a postcondition (result names the return value) — and also works mid-body as a checked assertion over locals.
  • invariant guards 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.

  • ? in a function whose failure type can’t carry the error → compile error listing the missing variant (fix with .map_err).
  • except arm 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 unwrap under @assure(gold) → rejected.
  • try/raise/finally unwinding — invisible to effect rows, hostile to replay and blame.
  • Go-style (value, err) tuples — handling is unenforced.
  • Silent Option-ization of failures — evidence loss.