Error handling

See also

New to statecharts? See Core concepts for an overview of how states, transitions, events, and actions fit together.

What happens when a callback raises an exception during a transition?

With StateChart, errors in actions are caught by the engine and dispatched as error.execution internal events — so the machine itself can react to failures by transitioning to an error state, retrying, or recovering. This follows the SCXML error handling specification.

Tip

catch_errors_as_events is a class attribute that controls this behavior. StateChart uses True by default (SCXML-compliant); set it to False to let exceptions propagate to the caller instead. See Behaviour for the full comparison of behavioral attributes and how to customize them.

How errors are caught

When an action raises during a microstep, the engine catches the exception at the block level. Each phase of the microstep is an independent block:

Block

Callbacks

Exit

on_exit_state(), on_exit_<state>()

On

on_transition(), on_<event>()

Enter

on_enter_state(), on_enter_<state>()

An error in one block:

  • Stops remaining actions in that block — per the SCXML spec, execution MUST NOT continue within the same block after an error.

  • Does not affect other blocks — subsequent phases of the microstep still execute. In particular, after callbacks always run regardless of errors in earlier blocks.

This means that even if a transition’s on action raises, the target states are still entered and after_<event>() callbacks still run. The error is caught and queued as an error.execution internal event that fires within the same macrostep.

Note

before callbacks run before any state changes, so an error in before prevents the transition from executing — but after still runs because it belongs to a separate block.

The error.execution event

After catching an error, the engine places an error.execution event on the internal queue. You can define transitions for this event to handle errors within the state machine itself — transitioning to error states, logging, or recovering.

The error_ naming convention

Since Python identifiers cannot contain dots, any event attribute starting with error_ automatically matches both the underscore and dot-notation forms. For example, error_execution matches both "error_execution" and "error.execution":

>>> from statemachine import State, StateChart

>>> class ResilientChart(StateChart):
...     operational = State(initial=True)
...     broken = State(final=True)
...
...     do_work = operational.to(operational, on="risky_action")
...     error_execution = operational.to(broken)
...
...     def risky_action(self):
...         raise RuntimeError("something went wrong")

>>> sm = ResilientChart()
>>> sm.send("do_work")
>>> "broken" in sm.configuration_values
True

Note

If you provide an explicit id= parameter on the Event, it takes precedence and the naming convention is not applied.

Accessing error data

The original exception is available as error in the keyword arguments of callbacks on the error.execution transition. Use dependency injection to receive it:

>>> from statemachine import State, StateChart

>>> class ErrorLogger(StateChart):
...     running = State(initial=True)
...     failed = State(final=True)
...
...     process = running.to(running, on="do_process")
...     error_execution = running.to(failed, on="log_error")
...
...     def do_process(self):
...         raise ValueError("bad data")
...
...     def log_error(self, error):
...         self.last_error = error

>>> sm = ErrorLogger()
>>> sm.send("process")
>>> str(sm.last_error)
'bad data'

Error in error handler

If the error.execution handler itself raises, the engine ignores the second error (logging a warning) to prevent infinite loops. The machine remains in whatever configuration it reached before the failed handler.

Note

During error.execution processing, errors in transition on content are not caught at block level — they propagate to the microstep where they are silently discarded. This prevents infinite loops when an error handler’s own action raises (e.g., a self-transition error_execution = s1.to(s1, on="handler") where handler raises). Entry/exit blocks still use block-level catching regardless of the current event.

Cleanup / finalize pattern

A common need is to run cleanup code after a transition regardless of success or failure — releasing a lock, closing a connection, or clearing temporary state.

Because errors are caught at the block level, after_<event>() callbacks always run — making them a natural finalize hook, similar to Python’s try/finally:

>>> from statemachine import Event, State, StateChart

>>> class ResourceManager(StateChart):
...     idle = State(initial=True)
...     working = State()
...     recovering = State()
...
...     start = idle.to(working)
...     done = working.to(idle)
...     recover = recovering.to(idle)
...     error_execution = Event(working.to(recovering), id="error.execution")
...
...     def __init__(self, should_fail=False):
...         self.should_fail = should_fail
...         self.released = False
...         super().__init__()
...
...     def on_enter_working(self):
...         if self.should_fail:
...             raise RuntimeError("something went wrong")
...         self.raise_("done")
...
...     def after_start(self):
...         self.released = True  # always runs — finalize hook
...
...     def on_enter_recovering(self, error):
...         self.last_error = error
...         self.raise_("recover")

On the success path, the machine transitions idle working idle and after_start releases the resource:

>>> sm = ResourceManager(should_fail=False)
>>> sm.send("start")
>>> "idle" in sm.configuration_values
True
>>> sm.released
True

On the failure path, the action raises, but after_start still runs. Then error.execution fires, transitions to recovering, and auto-recovers back to idle:

>>> sm = ResourceManager(should_fail=True)
>>> sm.send("start")
>>> "idle" in sm.configuration_values
True
>>> sm.released  # finalize ran despite the error
True
>>> str(sm.last_error)
'something went wrong'

See also

See Cleanup / finalize pattern for a more detailed version of this pattern with annotated output.

Validators do not trigger error events

Validators operate in the transition-selection phase, before any state changes occur. Their exceptions always propagate to the caller — they are never caught by the engine and never converted to error.execution events, regardless of the catch_errors_as_events setting.

This is intentional: a validator rejection means the transition should not happen at all. It is semantically equivalent to a condition returning False, but communicates the reason via an exception.

See also

See Validators for examples and the full semantics of validator propagation.

See also

See Behaviour for the full comparison of behavioral attributes and how to customize catch_errors_as_events and other settings. See Actions for the callback execution order within each microstep.