Child.Defect reports that an independently running child chart ended outside its modeled success
and error contracts. The event belongs to the mounted child occurrence, so supervision remains
explicit even when the failure began several runtime boundaries below the parent.
Supervision follows the chart tree.
A child occurrence turns unexpected failure inside an independently running chart into one source-qualified supervisory event. If that event remains unhandled, the same failure can cross the next child boundary.
1 · Supervise the child
The occurrence reports what its chart could not handle.
RenderEdition dies inside RenderJob. Because the child has no local RenderEdition.Defect listener, its Renderer occurrence completes with Renderer.Defect; Preparing handles that boundary event in place.
import { Activity, State, Statechart, States, Transitions } from "@motive/motive";
import { Schema } from "effect";
export const RenderEdition = Activity.make("RenderEdition", {
success: Schema.Struct({ artifactId: Schema.NonEmptyString }),
});
class RenderJob extends State.Compound<RenderJob>()("RenderJob") {
static states = States.make(() => [Rendering, Rendered]);
}
class Rendering extends State.Atomic<Rendering>()("Rendering") {
static transitions = Transitions.make(this, ({ invoke, on }) => [
invoke(RenderEdition),
on(RenderEdition.Done, Rendered, ({ event }) => event.value),
]);
}
class Rendered extends State.Done<Rendered>()(
"Rendered",
Schema.Struct({ artifactId: Schema.NonEmptyString }),
) {}
export const RenderJobChart = Statechart.make(RenderJob);
const Renderer = RenderJobChart.as("Renderer");
class Publication extends State.Compound<Publication>()("Publication") {
static states = States.make(() => [Preparing]);
}
class Preparing extends State.Atomic<Preparing>()("Preparing") {
static transitions = Transitions.make(this, ({ on, spawn }) => [
spawn(Renderer),
on(Renderer.Done),
on(Renderer.Defect),
]);
}
export const PublicationChart = Statechart.make(Publication);
2 · Escalate recursively
An unhandled child defect crosses the next boundary.
Publication deliberately leaves Renderer.Defect unhandled. Its Publisher occurrence therefore defects too, preserving the incident reference and representative defect for Workspace while it isolates the whole branch in Halted.
import { Activity, Incident, State, Statechart, States, Transitions } from "@motive/motive";
import { Schema } from "effect";
export const RenderEdition = Activity.make("RenderEdition", {
success: Schema.Struct({ artifactId: Schema.NonEmptyString }),
});
class RenderJob extends State.Compound<RenderJob>()("RenderJob") {
static states = States.make(() => [Rendering, Rendered]);
}
class Rendering extends State.Atomic<Rendering>()("Rendering") {
static transitions = Transitions.make(this, ({ invoke, on }) => [
invoke(RenderEdition),
on(RenderEdition.Done, Rendered, ({ event }) => event.value),
]);
}
class Rendered extends State.Done<Rendered>()(
"Rendered",
Schema.Struct({ artifactId: Schema.NonEmptyString }),
) {}
export const RenderJobChart = Statechart.make(RenderJob);
const Renderer = RenderJobChart.as("Renderer");
class Publication extends State.Compound<Publication>()("Publication") {
static states = States.make(() => [Preparing]);
}
class Preparing extends State.Atomic<Preparing>()("Preparing") {
static transitions = Transitions.make(this, ({ on, spawn }) => [
spawn(Renderer),
on(Renderer.Done),
]);
}
export const PublicationChart = Statechart.make(Publication);
const Publisher = PublicationChart.as("Publisher");
class Workspace extends State.Compound<Workspace>()("Workspace") {
static states = States.make(() => [Publishing, Halted]);
}
class Publishing extends State.Atomic<Publishing>()("Publishing") {
static transitions = Transitions.make(this, ({ on, spawn }) => [
spawn(Publisher),
on(Publisher.Defect, Halted, ({ event }) => ({
incident: event.incident,
defect: event.defect,
})),
]);
}
export class Halted extends State.Atomic<Halted>()("Halted", {
incident: Incident.Ref,
defect: Schema.Unknown,
}) {}
export const WorkspaceChart = Statechart.make(Workspace);
The child boundary owns the event
Renderer is an authored occurrence of RenderJobChart:
const Renderer = RenderJobChart.as("Renderer");
If that running child reaches State.Done, its occurrence produces Renderer.Done. If it reaches
State.Error, it produces Renderer.Error. If execution ends unexpectedly instead, the same site
produces Renderer.Defect with an incident reference and one representative defect.
The source-qualified event keeps three facts distinct: Renderer says which mounted boundary
reported the failure; event.incident names the immutable operational record; event.defect
provides a direct representative for authored control flow.
Internal defects become child defects when unhandled
The example's child invokes RenderEdition, but deliberately declares no
RenderEdition.Defect listener:
class Rendering extends State.Atomic<Rendering>()("Rendering") {
static transitions = Transitions.make(this, ({ invoke, on }) => [
invoke(RenderEdition),
on(RenderEdition.Done, Rendered, ({ event }) => event.value),
]);
}
When that Effect dies, RenderJob cannot publish a normal chart completion. Its mounted
Renderer occurrence therefore settles with Renderer.Defect. The parent does not need to know
which internal Activity failed in order to supervise the child as one component.
A local on(RenderEdition.Defect, ...) would give the child topology authority to isolate or
translate that failure before it reaches the chart boundary. Omitting it is a policy choice, not an
implicit conversion to modeled error.
Targetless handling retains the failed child
The first frame handles the occurrence event without leaving Preparing:
spawn(Renderer),
on(Renderer.Done),
on(Renderer.Defect),
The child is no longer running. Unlike the released Activity slot from the previous page, the terminal child outcome remains recorded beneath its active owner. That retained outcome makes the child boundary inspectable, but it does not restart the child or make the failure successful.
Reentering Preparing would retire the completed incarnation and mount a fresh child with a new
incarnation identity.
Unhandled defects propagate through child boundaries
The second frame mounts the same PublicationChart as another child occurrence:
const Publisher = PublicationChart.as("Publisher");
Publication does not handle Renderer.Defect. Its Publisher occurrence therefore ends with
Publisher.Defect, and Workspace becomes the next available supervisor:
spawn(Publisher),
on(Publisher.Defect, Halted, ({ event }) => ({
incident: event.incident,
defect: event.defect,
})),
This is recursive supervision, not ambient exception catching. Each hop is an authored child site with a precise lifetime, and each listener is selected by active topology.
Propagation preserves the incident
Crossing a child boundary does not replace the failure with a generic "child crashed" flag.
Publisher.Defect carries the incident reference and representative defect produced for the nested
failure. The example stores both in Halted, where the live readout confirms that the original
defect remains discoverable.
The next page examines Causes and defect channels directly. For now, the important rule is structural: preserve the incident reference until a deliberate supervisory or operational policy consumes it. The complete normalized Cause remains in incident storage rather than being copied through every chart boundary.
Supervision can stop at any authored boundary
An immediate parent may isolate one child, an outer parent may retire a whole subsystem, or a root engine may surface an unhandled defect to its operational boundary. The right listener is the narrowest boundary that can make an honest recovery or escalation decision.
Do not add a local listener merely to silence validation or logging. If a layer cannot respond meaningfully, allowing the defect to cross its occurrence boundary preserves authority for the supervisor that can.
Test the recursive boundary
- Defect both frames and compare the retained
Rendereroutcome with the exitedPublishersite. - Add
on(RenderEdition.Defect, ...)insideRenderJoband confirm that no child defect escapes. - Handle
Renderer.DefectinsidePublicationand confirm thatPublisher.Defectno longer reachesWorkspace. - Inspect
Haltedand verify that it retains the incident reference and original representative defect.
Next, Causes and defect channels separate the public supervisory event from the complete incident record it names.