Failure: status, not truth
ral keeps strictly apart two notions a shell conflates. Failure is a runtime
condition — a nonzero exit status or a raised error — that propagates
automatically until some form decides what to do with it. Truth is an ordinary
`Bool` value, inert data. ? and try react to failure;
if branches on truth; the two axes never cross.
A Bool is data, not a verdict. A ral predicate returns a Bool, and returning
false is a successful command: the value is simply the command’s value and
raises nothing, so false never becomes a failure and never drives ? or
try. Branching on a Bool is if’s job — it rejects a
non-Bool condition as a type error; branching on success is try’s.
? is a fallback chain: the first success wins. a ? b ? c tries each arm
left to right, returns the first that does not fail, and skips the rest; only a
failure falls through to the next arm. A non-failure escape — exit, a
job-control stop — propagates out immediately instead of being caught. If every arm fails the last error propagates, and all arms must
share one return type.
try turns failure into data, and is ral’s only ||. try B H runs B;
on success it returns B’s value, on failure it calls the handler H with an
error record [status, cmd, message, line, col] and returns its value,
unifying both outcomes into one type. It catches recoverable runtime errors and
nothing else — exit and job-control stops, the two Escapes, bypass it. When a
command encodes its result in its exit status rather than a value — an external
grep -q, say — try is how that success drives a branch:
try { grep -q p f; echo found } { |_| echo missing }. There is no
command-level ||: it would force precedence against ? and | for a case
try already covers. The only || is the short-circuiting Bool connective
inside $[…] expression blocks (docs/SPEC.md §17.1), which takes strict Bool
operands and never inspects command success.
The cleanup forms differ only in what they do with the original failure:
trysuppresses it;guard B Cruns cleanupCwhether or notBfailed, and letsB’s failure keep propagating — unlessChalts as well, because any halt of the cleanup pre-empts the body’s outcome, an error exactly as an escape. So:Breturns andChalts, the halt is the outcome; both halt,C’s signal wins, whichever kinds the two are;Bhalts andCreturns,B’s signal survives. Log-and-continue is one keystroke away —guard B { try C { |e| … } }— and the primitive stays strict (a-failing-cleanup-pre-empts);- the prelude’s
attemptdiscards both result and failure.
The kernel model carries both forms as of 2026-08-19. In dev/agda a failure is
a term — halt, spelled fail when it carries an error and exit when it
carries an escape — and every stack frame answers one. try and guard are the
two frames that answer with more than a pop, and the pair of rules βtry-err /
βtry-esc is the whole of “catches recoverable runtime errors and nothing
else”: one pattern match on the signal, catching the error and letting the
escape walk past. guard is not sugar for try there either — it reduces to
its cleanup resumed in front of the body’s outcome (βguard-halt, the detour
through ؛), so a cleanup that halts pre-empts that outcome, which is the rule
above read on the other side: one rule for both signals, and the shell and the
calculus now say it in the same words. ? needs no form of its own there: it is
try with a handler that drops the error it binds, and the model proves the
equation that makes that a definition rather than a resemblance — fail W ? N
is N.
Failure propagates predictably through the rest of the grammar:
- a sequence
a; b; chalts at the first failure, and the error says how many later parts it abandoned — otherwise the truncation is legible only as a shortchildrenlist in an enclosingaudit, which reads the same as a sequence that had fewer parts; - a pipeline fails whole on any stage failure, bar a stage ral itself ended because its reader was gone;
for/mapstop iterating on a failing body;spawncaptures failure in the handle and surfaces it onawait;- an unhandled failure at top level exits the process with that status.
See also syscalls-are-effects (failure is an operation’s exceptional outcome),
pipelines,
control-operators, types,
cbpv.
Cite: RATIONALE §“Failure is not truth”, §“Pipelines follow their edges”;
docs/SPEC.md §2.5, §8, §8.6–§8.7; the Try / Guard arms and frames in
core/src/evaluator/machine.rs (? elaborates to nested try, no frame of
its own), try / guard typing in core/src/typecheck/scope.rs.