Error handling – Quest Recovery

This example demonstrates error.execution handling using StateChart.

When catch_errors_as_events=True (the StateChart default), runtime errors in callbacks are caught and dispatched as error.execution events instead of propagating as exceptions. This lets you define error-recovery transitions.

  • The error_ naming convention auto-registers both error_X and error.X event names.

  • Alternatively, use Event(transitions, id="error.execution") for explicit registration.

  • Error data (the original exception, event, etc.) is available in handler kwargs.

from statemachine import Event
from statemachine import State
from statemachine import StateChart


class QuestRecoveryMachine(StateChart):
    """A quest where actions can fail and the error handler routes to recovery.

    When ``on_enter_danger_zone`` raises, the ``error.execution`` event fires
    and transitions to the ``recovering`` state instead of crashing.
    """

    safe = State("Safe", initial=True)
    danger_zone = State("Danger Zone")
    recovering = State("Recovering")
    completed = State("Quest Complete", final=True)

    venture = safe.to(danger_zone)
    survive = danger_zone.to(completed)
    recover = recovering.to(safe)

    # Register error.execution handler using Event with explicit id
    error_execution = Event(
        safe.to(recovering) | danger_zone.to(recovering),
        id="error.execution",
    )

    def on_enter_danger_zone(self):
        # This simulates an unexpected error during a quest action
        raise RuntimeError("Ambush! Orcs attack!")

    def on_enter_recovering(self, error=None, **kwargs):
        if error:
            print(f"Error caught: {error}")
        print("Retreating to recover...")
  • statechart error handling machine
  • statechart error handling machine

Error triggers recovery instead of crashing

When entering danger_zone raises a RuntimeError, the error is caught and dispatched as error.execution. The machine transitions to recovering.

sm = QuestRecoveryMachine()
print(f"Start: {sorted(sm.configuration_values)}")
assert "safe" in sm.configuration_values

sm.send("venture")
print(f"After venture: {sorted(sm.configuration_values)}")
assert "recovering" in sm.configuration_values
Start: ['safe']
Error caught: Ambush! Orcs attack!
Retreating to recover...
After venture: ['recovering']

Recover and try again

sm.send("recover")
print(f"After recovery: {sorted(sm.configuration_values)}")
assert "safe" in sm.configuration_values
After recovery: ['safe']

Comparison with catch_errors_as_events=False (error propagation)

With catch_errors_as_events=False, the same error would propagate as an exception instead of being caught.

class QuestNoCatch(StateChart):
    catch_errors_as_events = False

    safe = State("Safe", initial=True)
    danger_zone = State("Danger Zone", final=True)

    venture = safe.to(danger_zone)

    def on_enter_danger_zone(self):
        raise RuntimeError("Ambush! Orcs attack!")


sm2 = QuestNoCatch()
try:
    sm2.send("venture")
except RuntimeError as e:
    print(f"Exception propagated: {e}")
Exception propagated: Ambush! Orcs attack!