Error Handling
Failures are data, not exceptions. A throwing handler, rejected promise, or
unhandled internal event never escapes send(). The actor enters the built-in
terminal __error state and records the failure in snapshot().error.
No-exception rule
Section titled “No-exception rule”send()never throws. Worst case: machine dies to__error.- Subscribers that throw are swallowed; machine and other subscribers carry on.
- Constructor is the only exception. Bad setup throws immediately (programmer error, not runtime).
__error state
Section titled “__error state”Synthesized by the actor, not in your states list. Terminal.
- Thrown value recorded alongside last state, context, triggering event, and
ErrorReason - All queued events dropped, pending effects aborted
- Every subsequent
sendis a no-op snapshot().doneistrue;changeanddonelifecycle events fire
snapshot().error
Section titled “snapshot().error”undefined while alive. When set, carries ErrorInfo:
| Field | Meaning |
|---|---|
error | thrown value or rejection |
state | last known good state |
context | context at that state |
event | triggering event (__init at construction) |
reason | ErrorReason tag |
ErrorReason values: transition, effect, unhandled, budget, output,
internal, async.
Prefer catch and emit
Section titled “Prefer catch and emit”__error kills the machine. Better: catch, emit an internal error event, let a
transition handle it. The error state can leave via a back handler or
similar.
m.effect(submitting, { name: "chargeCard", fn: ({ signal, emit }) => { chargeCard(cardNumber) .then((id) => { if (!signal.aborted) emit(paymentOk.create({ orderId: id })); }) .catch((r) => { if (!signal.aborted) emit(paymentFail.create({ reason: String(r) })); }); },});on("error")
Section titled “on("error")”Subscribe to death. Fired with the full ErrorInfo. Seeded: if the machine
died before you subscribed, the last ErrorInfo replays immediately.
checkout.on("error", (info) => { console.error(`${info.reason} on ${info.event.type}:`, info.error);});recover()
Section titled “recover()”Bring a dead machine back. Caller supplies state and context. Breaks determinism (no longer “same inputs, same trace”). Effects not re-run, timers not re-armed. No-op on a live machine.
checkout.recover({ state: payment, context: { paymentInfo: { cardNumber: "..." } } });