temporalio/temporal · error

ErrInvalidTransition

ErrInvalidTransition

Error message

%w from %v

What it means

Transition.Apply in the CHASM state machine library returns this error when the current state of a StateMachine is not among the transition's declared Sources. It wraps the sentinel ErrInvalidTransition (a FailedPrecondition serviceerror) with the previous state value so callers can see which state blocked the transition. The library throws it to prevent applying an event whose transition is undefined from the current state.

Source

Thrown at chasm/statemachine.go:71

	prevState := sm.StateMachineState()

	// Defer to always emit the transition telemetry event.
	if telemetry.DebugMode() {
		defer func() {
			attrs := []attribute.KeyValue{
				attribute.String("chasm.transition.source", fmt.Sprintf("%v", prevState)),
				attribute.String("chasm.transition.destination", fmt.Sprintf("%v", t.Destination)),
			}
			if retErr != nil {
				attrs = append(attrs, attribute.String("chasm.transition.error", retErr.Error()))
			}
			span := trace.SpanFromContext(ctx.goContext())
			span.AddEvent("chasm.transition", trace.WithAttributes(attrs...))
		}()
	}

	if !t.Possible(sm) {
		return fmt.Errorf("%w from %v", ErrInvalidTransition, prevState)
	}

	if err := t.apply(sm, ctx, event); err != nil {
		return err
	}
	sm.SetStateMachineState(t.Destination)
	return nil
}

View on GitHub (pinned to bde624efd1)

Solutions

  1. Check sm.StateMachineState() (or Possible) before Apply and treat the invalid transition as an expected no-op where idempotent
  2. Add the missing state to the transition's Sources if the state is legitimately reachable
  3. Use errors.Is(err, chasm.ErrInvalidTransition) to distinguish expected invalid transitions from real apply failures and log/alert only on unexpected ones

Example fix

// before
err := transition.Apply(sm, ctx, event)
if err != nil {
  return err
}
// after
if !transition.Possible(sm) {
  return nil // already transitioned; idempotent no-op
}
err := transition.Apply(sm, ctx, event)
if err != nil {
  return err
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Go
if !transition.Possible(sm) {
  // expected: skip or no-op
  return nil
}

Type guard

func canApply[S comparable, SM interface{ StateMachineState() S }](t TransitionLike[S,SM], sm SM) bool { return slices.Contains(t.Sources(), sm.StateMachineState()) }

Try / catch

if err := t.Apply(sm, ctx, event); err != nil {
  if errors.Is(err, chasm.ErrInvalidTransition) {
    logger.Info("transition not valid from current state", "state", sm.StateMachineState())
    return nil // idempotent path
  }
  return err
}

Prevention

When it happens

Trigger: Calling Transition.Apply(sm, ctx, event) when sm.StateMachineState() is not in t.Sources (checked via Possible), i.e. applying an event to a state machine in a state the transition was not declared for.

Common situations: Double-processing an event that already moved the machine (e.g. replayed history or a duplicated task), transition tables missing a legitimate source state, or concurrent handlers racing to transition the same machine where only one expected path is valid.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of temporalio/temporal@bde624efd1 (2026-09-01). Data as JSON: /api/errors/7eb65ee41fa46abc. Report an issue: GitHub.