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
- Check sm.StateMachineState() (or Possible) before Apply and treat the invalid transition as an expected no-op where idempotent
- Add the missing state to the transition's Sources if the state is legitimately reachable
- 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
- Always check Possible() before Apply when the transition is expected to be conditional
- Design transitions to be idempotent for replayed/duplicated events
- Use errors.Is with ErrInvalidTransition to classify expected vs unexpected failures
- Enumerate all reachable source states when defining transition tables
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
- failed to deserialize component: %w
- failed to deserialize data: %w
- %w: zombie workflow cannot be updated
- ErrStaleReference
- %w: %w
AI-assisted analysis of temporalio/temporal@bde624efd1 (2026-09-01).
Data as JSON: /api/errors/7eb65ee41fa46abc.
Report an issue: GitHub.