State.Error is a terminal leaf for modeled failure. Like State.Done, it completes the state that
owns it. Unlike State.Done, it preserves a typed failure as an Effect Cause.
Make failure an explicit outcome.
State.Error marks one boundary as failed and preserves its Schema-owned reason. At the root it ends the instance; nested beneath a compound state it gives that parent an error to handle.
1 · Names describe; state kinds behave
A state named Failed may still be ordinary.
The job enters a leaf named Failed, but that leaf is State.Atomic. The job remains Running, and the event's reason is not part of its terminal result.
import { State, Statechart, States, Transitions } from "@motive/motive";
import { Schema } from "effect";
const FailJob = Schema.TaggedStruct("FailJob", { reason: Schema.String });
export class Job extends State.Compound<Job>()("Job") {
static states = States.make(() => [Working, Failed]);
}
class Working extends State.Atomic<Working>()("Working") {
static transitions = Transitions.make(this, ({ on }) => [
on(FailJob, Failed),
]);
}
class Failed extends State.Atomic<Failed>()("Failed") {}
export const JobChart = Statechart.make(Job);
2 · Fail the root
State.Error ends the instance with a typed cause.
Failed now declares Schema.String as its error type. The transition supplies the event's reason; entering this root-level error state produces an Error snapshot that preserves it.
import { State, Statechart, States, Transitions } from "@motive/motive";
import { Schema } from "effect";
const FailJob = Schema.TaggedStruct("FailJob", { reason: Schema.String });
export class Job extends State.Compound<Job>()("Job") {
static states = States.make(() => [Working, Failed]);
}
class Working extends State.Atomic<Working>()("Working") {
static transitions = Transitions.make(this, ({ on }) => [
on(FailJob, Failed, ({ event }) => event.reason),
]);
}
class Failed extends State.Error<Failed>()("Failed", Schema.String) {}
export const JobChart = Statechart.make(Job);
3 · Handle a nested error
A parent may turn failure into its next state.
Failed now completes only Attempt with an error. State.error(this) lets Attempt handle that cause and move the still-running job to NeedsAttention.
import { State, Statechart, States, Transitions } from "@motive/motive";
import { Schema } from "effect";
const FailJob = Schema.TaggedStruct("FailJob", { reason: Schema.String });
export class Job extends State.Compound<Job>()("Job") {
static states = States.make(() => [Attempt, NeedsAttention]);
}
class Attempt extends State.Compound<Attempt>()("Attempt") {
static states = States.make(() => [Working, Failed]);
static transitions = Transitions.make(this, ({ on }) => [on(State.error(this), NeedsAttention)]);
}
class Working extends State.Atomic<Working>()("Working") {
static transitions = Transitions.make(this, ({ on }) => [
on(FailJob, Failed, ({ event }) => event.reason),
]);
}
class Failed extends State.Error<Failed>()("Failed", Schema.String) {}
class NeedsAttention extends State.Atomic<NeedsAttention>()("NeedsAttention") {}
export const JobChart = Statechart.make(Job);
A failure-shaped name is not an error result
The first frame enters an ordinary State.Atomic named Failed. The job remains Running because
names carry domain meaning, not terminal behavior. The reason field arrived on FailJob, but this
model does not preserve it as a chart result.
Use an ordinary atomic failure state when failure is simply another running phase with more events
to handle. Use State.Error when the owning boundary must finish unsuccessfully.
Error at the root ends the instance
The second frame changes Failed to State.Error and gives it Schema.String as its error Schema.
The transition into Failed must now construct that typed value, so it returns event.reason.
Because Failed belongs directly to the root Job, entering it terminates the chart with an Error
snapshot. Its cause retains the modeled string as a failure reason. Consumers can distinguish a
successful Done result from a modeled Error without parsing state names or logs.
State.Error is not an exception handler. It is authored terminal geometry for a failure the domain
has chosen to represent.
Error inside a compound state completes that state
The final frame nests Working and Failed beneath Attempt. Entering Failed now completes
Attempt with an error and produces State.error(Attempt)—written inside its transition declaration
as State.error(this).
Attempt handles that completion by moving the root job to NeedsAttention. The cause crossed the
nested boundary, but the root never entered one of its own error states, so the job remains
Running. Handling, recovery, and escalation are parent decisions; terminal failure belongs to the
boundary that owns the error state.
Test the boundary
- Remove the
State.error(this)transition and identify which nested state remains terminal. - Change
NeedsAttentionto a root-levelState.Errorand supply the nested cause to it. - Replace
Schema.Stringwith a tagged domain error and follow the new required value through the transition.