Guide

Modeled Error and Defect

Keep expected domain failure distinct from unexpected supervisory failure.

An Activity can fail in two fundamentally different ways. A modeled error is an expected result named by the Activity contract. A defect is an unexpected failure that belongs to supervision.

Failure keeps its authority.

An expected failure belongs to the Activity's typed contract. A defect does not. Motive preserves that boundary so domain recovery and supervision remain different decisions.

1 · Expected failure

A modeled error returns to the domain.

ReviewOrder fails with the ReviewUnavailable value declared by its error Schema. ReviewOrder.Error carries that value back into transition selection, where the chart enters NeedsReview.

import { Activity, State, Statechart, States, Transitions } from "@motive/motive";
import { Schema } from "effect";

const ReviewUnavailable = Schema.TaggedStruct("ReviewUnavailable", {
  reason: Schema.String,
});

const ReviewOrder = Activity.make("ReviewOrder", {
  input: Schema.Struct({ orderId: Schema.String }),
  success: Schema.Struct({ approvalCode: Schema.String }),
  error: ReviewUnavailable,
});

export class Review extends State.Compound<Review>()("Review") {
  static states = States.make(() => [Checking, NeedsReview]);
}

class Checking extends State.Atomic<Checking>()("Checking") {
  static transitions = Transitions.make(this, ({ invoke, on }) => [
    invoke(ReviewOrder, () => ({ orderId: "order-123" })),
    on(ReviewOrder.Done),
    on(ReviewOrder.Error, NeedsReview),
  ]);
}

class NeedsReview extends State.Atomic<NeedsReview>()("NeedsReview") {}

export const ReviewChart = Statechart.make(Review);

2 · Unexpected failure

A defect returns to supervision.

The same Activity implementation now dies outside its declared error contract. ReviewOrder.Defect carries an incident reference and representative defect to the supervisory listener, which isolates the order in Halted.

import { Activity, State, Statechart, States, Transitions } from "@motive/motive";
import { Schema } from "effect";

const ReviewUnavailable = Schema.TaggedStruct("ReviewUnavailable", {
  reason: Schema.String,
});

const ReviewOrder = Activity.make("ReviewOrder", {
  input: Schema.Struct({ orderId: Schema.String }),
  success: Schema.Struct({ approvalCode: Schema.String }),
  error: ReviewUnavailable,
});

export class Review extends State.Compound<Review>()("Review") {
  static states = States.make(() => [Checking, NeedsReview, Halted]);
}

class Checking extends State.Atomic<Checking>()("Checking") {
  static transitions = Transitions.make(this, ({ invoke, on }) => [
    invoke(ReviewOrder, () => ({ orderId: "order-123" })),
    on(ReviewOrder.Done),
    on(ReviewOrder.Error, NeedsReview),
    on(ReviewOrder.Defect, Halted),
  ]);
}

class NeedsReview extends State.Atomic<NeedsReview>()("NeedsReview") {}

class Halted extends State.Atomic<Halted>()("Halted") {}

export const ReviewChart = Statechart.make(Review);

Expected failure belongs to the model

ReviewOrder names an error Schema alongside its input and success Schemas:

error: Schema.TaggedStruct("ReviewUnavailable", {
  reason: Schema.String,
});

This says that an unavailable review service is a result the application understands. When the implementation fails with that decoded value, the occurrence produces ReviewOrder.Error.

Error carries a typed value and its Cause

An Error event exposes the modeled failure as event.error and preserves the complete Effect Cause as event.cause. Most domain responses begin with the typed value: retry later, request a manual review, decline an operation, or retain an explanation.

The first frame chooses NeedsReview:

on(ReviewOrder.Error, NeedsReview);

That transition is application policy. The Activity reports an expected outcome; it does not choose the state that should own the response.

Defect stays outside the error Schema

The same implementation can die because an invariant was violated, a dependency returned an impossible value, or code failed somewhere the domain contract did not anticipate. Motive does not widen ReviewUnavailable to make that failure fit.

Instead, a terminal Cause containing a defect produces ReviewOrder.Defect. The event carries an opaque event.incident reference and one event.defect representative; it has no invented error value. The immutable incident record retains the complete normalized Cause and its exact provenance for operational inspection.

Supervision is explicit topology

The second frame keeps the modeled Error disposition and adds a supervisory edge:

on(ReviewOrder.Defect, Halted);

Halted is not another spelling of NeedsReview. It says the order has left its ordinary domain recovery path and now requires a supervisory decision. Another application might escalate, isolate a subtree, record an incident, or deliberately terminate the instance.

One Effect Cause, two authorities

Both buttons settle a real running Effect. The first uses the typed error channel; the second uses a Cause containing a defect. Motive inspects the terminal Cause and preserves the distinction at the statechart boundary:

  • a Cause with a declared Activity failure and no defect returns through Error;
  • a Cause containing a defect returns through Defect.

Interruption, mixed Causes, and the exact information preserved by each channel deserve their own treatment. The important rule here is simpler: do not make an unexpected failure look expected by forcing it into the Activity's error Schema.

Modeled outcomes are total; supervision is optional

Because ReviewOrder declares success and error Schemas, an active invocation must provide an eligible disposition for both Done and Error. Those are known branches of its contract.

Defect remains available even for an Activity whose modeled error type is impossible, but its disposition is supervisory and optional. Whether an unhandled defect terminates, escalates, or is required to be caught is a supervision policy—not a fabricated domain outcome.

Test the boundary

  1. Settle both frames and compare the selected edge, destination, and authority readout.
  2. Remove on(ReviewOrder.Error, NeedsReview) and observe that the declared modeled outcome is no longer fully dispositioned.
  3. Remove on(ReviewOrder.Defect, Halted) and compare the resulting supervisory behavior without changing the Activity's error Schema.

The next page narrows in on Activity.Defect: its incident evidence, its lifetime, and the policies available to the state that supervises the occurrence.