Skip to content

Effects

An effect is a function that runs every time the actor enters a state. It does side-effect work and emits events back into the machine. It does not transition the machine directly.

{
signal: AbortSignal;
state: { name: string; payload: unknown };
event: InternalEvent;
context: Context<ActorContext>;
emit: (event) => void;
clock: Clock;
}
  • Run on every entry, initial state included (event.type is "__init" at construction)
  • Terminal states run entry effects too (emit completion outputs to parents)
  • Return value is ignored. Effects emit; the machine reacts.
  • Never throw. Catch and emit an internal error event instead.

signal fires when the actor leaves the state. Check it before emitting.

m.effect(submitting, {
name: "chargeCard",
fn: ({ signal, emit }) => {
chargeCard(cardNumber).then((orderId) => {
if (!signal.aborted) emit(paymentOk.create({ orderId }));
});
},
});

Post-abort emit is a silent no-op anyway. The guard skips the wasted call.

Call m.effect multiple times for one state. They run in order.

m.effect(submitting, {
name: "chargeCard",
fn: ({ signal, emit }) => {
chargeCard(cardNumber).then(
(id) => {
if (!signal.aborted) emit(paymentOk.create({ orderId: id }));
},
(err) => {
if (!signal.aborted) emit(paymentFail.create({ reason: String(err) }));
},
);
},
});
m.effect(submitting, {
name: "startSubmitTimeout",
fn: ({ signal, emit, clock }) => {
clock.setTimeout(800, () => {
if (!signal.aborted) emit(submittingDone.create());
});
},

An uncaught error moves the actor to the built-in terminal __error state. snapshot().done becomes true, snapshot().error carries the failure. The actor stops accepting events.