Every failed Effect ends with a Cause: a structured value that preserves all the reasons the
computation failed. Motive uses the whole Cause to classify an outcome. Modeled Error events
expose it directly; supervisory Defect events record it in immutable incident storage and carry
the incident reference plus one representative defect.
A Cause is more than one error.
A Cause preserves every reason an Effect failed. Motive classifies that complete value at an Activity or child boundary, then exposes the channel that matches its strongest reason.
1 · Project modeled failure
Error gives one typed reason a domain name.
Review fails with a single typed Fail reason. Review.Error exposes that reason as event.error while retaining the complete one-reason Cause as event.cause.
import { Activity, State, Statechart, States, Transitions } from "@motive/motive";
import { Schema } from "effect";
export const ReviewUnavailable = Schema.TaggedStruct("ReviewUnavailable", {
service: Schema.NonEmptyString,
});
const Review = Activity.make("Review", {
success: Schema.Void,
error: ReviewUnavailable,
});
class ReviewFlow extends State.Compound<ReviewFlow>()("ReviewFlow") {
static states = States.make(() => [Checking, Approved, NeedsReview]);
}
class Checking extends State.Atomic<Checking>()("Checking") {
static transitions = Transitions.make(this, ({ invoke, on }) => [
invoke(Review),
on(Review.Done, Approved),
on(Review.Error, NeedsReview, ({ event }) => ({
error: event.error,
cause: event.cause,
})),
]);
}
class Approved extends State.Atomic<Approved>()("Approved") {}
export class NeedsReview extends State.Atomic<NeedsReview>()("NeedsReview", {
error: ReviewUnavailable,
cause: Schema.Cause(ReviewUnavailable, Schema.Never),
}) {}
export const ReviewFlowChart = Statechart.make(ReviewFlow);
2 · Classify the whole Cause
One defect keeps a mixed Cause supervisory.
The same Activity now ends with both Fail and Die reasons. Because the complete Cause contains a defect, Review.Defect—not Review.Error—is selected; Halted retains the incident reference and representative defect while incident storage retains both reasons.
import { Activity, Incident, State, Statechart, States, Transitions } from "@motive/motive";
import { Schema } from "effect";
export const ReviewUnavailable = Schema.TaggedStruct("ReviewUnavailable", {
service: Schema.NonEmptyString,
});
const Review = Activity.make("Review", {
success: Schema.Void,
error: ReviewUnavailable,
});
class ReviewFlow extends State.Compound<ReviewFlow>()("ReviewFlow") {
static states = States.make(() => [Checking, Approved, NeedsReview, Halted]);
}
class Checking extends State.Atomic<Checking>()("Checking") {
static transitions = Transitions.make(this, ({ invoke, on }) => [
invoke(Review),
on(Review.Done, Approved),
on(Review.Error, NeedsReview, ({ event }) => ({
error: event.error,
cause: event.cause,
})),
on(Review.Defect, Halted, ({ event }) => ({
incident: event.incident,
defect: event.defect,
})),
]);
}
class Approved extends State.Atomic<Approved>()("Approved") {}
class NeedsReview extends State.Atomic<NeedsReview>()("NeedsReview", {
error: ReviewUnavailable,
cause: Schema.Cause(ReviewUnavailable, Schema.Never),
}) {}
export class Halted extends State.Atomic<Halted>()("Halted", {
incident: Incident.Ref,
defect: Schema.Unknown,
}) {}
export const ReviewFlowChart = Statechart.make(ReviewFlow);
Cause preserves failure structure
In the installed Effect version, a Cause contains a reasons collection. Each reason is one of
three kinds:
Fail<E>carries a typed error from the Effect's expected error channel.Diecarries an unexpected defect.Interruptrecords which fiber interrupted the computation.
A Cause may contain more than one reason. Concurrency, cleanup, and composed failure can therefore
retain facts that a single Error value cannot represent.
Error is a projection of a modeled Cause
The first frame settles Review with one typed ReviewUnavailable failure. With no defect in the
Cause, Motive selects Review.Error.
That event exposes two related values:
on(Review.Error, NeedsReview, ({ event }) => ({
error: event.error,
cause: event.cause,
}));
event.error is the convenient typed projection for ordinary domain decisions. event.cause is
the complete modeled Cause. The projection does not replace its authority.
Defect classification examines the whole Cause
The second frame ends with a Cause containing both a typed Fail and an unexpected Die. Motive
checks the complete Cause, finds the defect, and selects Review.Defect:
on(Review.Defect, Halted, ({ event }) => ({
incident: event.incident,
defect: event.defect,
}));
The typed failure reason is not discarded: it remains in the incident record's normalized Cause.
But the public event is not weakened to Review.Error merely because a modeled reason is also
present. Any retained defect keeps the terminal outcome under supervisory authority.
The channel is a classification, not a second failure
Review.Error and Review.Defect do not wrap the Cause in new exceptions. They describe how the
mounted occurrence classified its terminal Cause and which authored listener may respond. Error
keeps the typed projection and Cause close to domain control flow; Defect keeps the complete Cause
under incident authority and gives the chart only its reference and representative.
This is why a defect can cross child boundaries without accumulating strings such as "child failed" and "parent failed." The occurrence source changes at each boundary; the incident remains the authority for the structured Cause.
Interruption belongs to lifecycle
An Interrupt reason records cancellation of the running Effect. A pure interruption is not
promoted into Activity.Defect: leaving the owning state, stopping an instance, and retiring a
child are lifecycle operations, not evidence that the implementation died unexpectedly.
A Cause may retain interruption alongside other reasons. Classification still responds to the
whole value: a contained Die remains a defect, and a typed Fail remains available for modeled
inspection when no defect is present.
Inspect without flattening
Effect provides structural operations such as Cause.hasDies, Cause.hasFails,
Cause.findDefect, and Cause.findErrorOption. Use them directly with a modeled Error Cause, or
after retrieving a supervisory incident record at the operational policy boundary that needs to
decide, format, or retain a specific fact. Use event.defect when authored control flow needs only
the representative unexpected value.
Avoid reducing a Cause to String(cause) at the first listener. Human-readable formatting is a
projection for logs or UI; it is not a replacement for typed reasons, annotations, interruption
identity, or the original defect value.
Test the classification boundary
- Settle both frames and compare the one-reason Error Cause with the Defect event's incident reference and representative.
- Remove the
Diereason from the second frame and confirm thatReview.Errorbecomes eligible. - Add a second typed Fail reason and verify that the complete Cause retains both even though
event.errorremains one convenient projection. - Retrieve the incident record, encode and decode its Cause, and confirm both reason kinds survive the durable boundary.
Next, retry policy decides which modeled failures may attempt the same occurrence again—and what happens when that policy is exhausted.