Errors
You signal failure by throwing (or rejecting, or Effect.fail). Where the failure surfaces depends on which hook threw. Failing acquire is a readiness error; failing refresh or an action is an operation failure against a node that stays ready.
| Hook fails | Result | Caller sees |
|---|---|---|
acquire | Node enters readiness error (Error). | ensureReady() rejects; React useNode throws to the error boundary. |
refresh | Node stays Ready, result rolled back. | operationFailure recorded; the current result is unchanged. |
| action | Node stays Ready. | The action rejects; operationFailure recorded. |
Failing acquire
A throw or rejection in acquire puts the node into a readiness error. There is no result to show.
acquire: Frond.Driver.Acquire(async (ctx) => {
const res = await fetch(`/api/profile/${ctx.args.userId}`, { signal: ctx.signal });
if (!res.ok) throw new ProfileLoadError(res.status);
return res.json();
}),
Outside React, ensureReady() rejects with the error. In React, useNode throws it to the nearest error boundary. The runtime does not retry on its own — retry by calling ensureReady() again, which starts a fresh attempt. See Lifecycle.
Cause chains
Frond wraps failures as they cross runtime boundaries. The outer wrapper explains what failed in Frond; the inner cause is the failure your driver or dependency produced.
For the ProfileLoadError above, a readiness failure can look like this when serialized:
0 FrondRuntimeReadError kind=readiness nodeId=resources/profile:v1:{"userId":"u_42"}
1 AcquireFailed nodeTag=resources/profile
2 ProfileLoadError message="Profile request failed with 500"
The first frame is useful to React and the runtime: it says this was a retryable readiness read for a specific node. The second frame says the acquire hook failed. The last frame is the domain error that should usually be visible in a report.
Dependency failures add more graph context:
0 FrondRuntimeReadError kind=readiness nodeId=resources/dashboard:v1:{}
1 DependencyFailed dependency=profile dependencyNodeId=resources/profile:v1:{"userId":"u_42"}
2 AcquireFailed nodeTag=resources/profile
3 ProfileLoadError message="Profile request failed with 500"
Do not flatten this too early. Product control flow needs the outer wrapper; diagnostics and reporting need the full chain. Error projection is the reporting boundary that selects the meaningful root cause while preserving the Frond context.
Failing refresh or an action
A failure in refresh or an action does not poison readiness. The node keeps its current result and the runtime records an operation failure.
refreshrolls back to the previous result on failure.- An action rejects to its caller (
node.actions.rename(...)throws;handle.runAction(...)returns aFailureresult). A runtime-managedctx.setResult(...)commit is applied only when the action succeeds; direct MobX mutations you perform before throwing are your responsibility.
Read the last failure from useNodeState:
const state = FrondReact.useNodeState(ProfileNode, { userId });
state.operationFailure; // the last operation failure, if any
operationFailure.error is the runtime failure value. Project it before showing it in UI diagnostics or sending it to a tracker:
if (state.operationFailure) {
const report = Frond.Diagnostics.createErrorReport(state.operationFailure.error);
report.message; // presentable failure summary
}
This is the state-based version of React boundary projection. Readiness failures are thrown to the boundary and use getErrorReport(error); action and refresh failures stay on the ready node and use Frond.Diagnostics.createErrorReport(operationFailure.error).
Timeouts
Every driver hook runs under a timeout. A hook that exceeds it fails like any other failure — an acquire timeout is a readiness error, a refresh or action timeout is an operation failure.
| Hook | Default |
|---|---|
acquire | 15s |
refresh | 15s |
| action | 15s |
live | 15s |
release | 5s |
Override them on the runtime:
const runtime = Frond.createRuntime({
driverTimeouts: { acquire: 30_000, action: 10_000 },
});
Cancellation
When work is cancelled — a newer attempt supersedes it, or the node is released or evicted — ctx.signal aborts. Pass the signal to cancellable work so it stops promptly. A late result from cancelled work is not committed, so you do not need to guard against it. Cancellation is not a domain error; do not report it as one.
Next: Error projection — why Frond unwraps runtime failures before display or reporting.