Debugging states
When state rules grow, it becomes important to understand:
- Which states are currently active?
- Why did a particular state activate?
- Why did another one not activate?
- Which transitions actually ran?
- Which targets were shown or hidden?
Both
StateEngine
and
DisplayStateEngine
provide built-in debugging,
but they are enabled differently.
DisplayStateEngine debugging
DisplayStateEngine
accepts configuration options as the first element
of
static display_states. This includes a
debug
option, which
is an array specifying what to log. Here's how you enable debugging:
static display_states = [
{
visibility_mode: "whitelist",
debug: [
"active_states",
"displayed_targets",
"hidden_targets",
"missing_targets",
"timing"
]
},
[ { status: "draft" }, ["#draft"] ],
[ { status: "paid" }, ["#paid"] ],
[ { status: "cancelled" }, ["#cancelled"] ],
];
Now, whenever fields or flags change, the engine will print structured information to the console. Depending on what you enable, you may see:
- Which states matched.
- Which states became active or inactive.
- Which targets are about to be shown.
- Which targets are about to be hidden.
- Warnings about missing targets.
- Timing information for transitions.
Because debug is selective, you can keep output focused instead of noisy.
It also helps to provided a
name
in rule metadata:
[ { status: "paid" }, (c) => c.generateInvoice(), { name: "paid" } ]
That name appears in logs, making output easier to read. If you don't provide the name (which, admittedly, can make your state rules a bit noisy and appear longer than they need to be), debugger will print rule sequence number instead. However, a sequence number might be enough to identify the offending rule without relying on names. The only caveat with sequence number automatically assigned to rules is to remember how nested rules are processed.
StateEngine debugging
StateEngine
supports debugging in the same way as
DisplayStateEngine
: you
provide a
debug
option as the first element of the
static states = [...]
array. The
debug
option is an array specifying what to log:
static states = [
{
debug: [
"active_states",
"enter_states",
"exit_states",
"eligible_transitions",
"applied_transitions",
"skipped_transitions",
"timing",
]
},
[ { status: "paid" }, (c) => c.generateInvoice(), { name: "paid" } ],
[ { status: "cancelled" }, (c) => c.handleCancel(), { name: "cancelled" } ],
];
Now, whenever fields or flags change,
StateEngine
will print structured
information to the console for the categories you enabled. Because debug is
selective, you can keep output focused instead of noisy.
Nested rules in debug output
Nested rules appear in logs as independent states. This is because internally, nested rules are expanded into standalone states with inherited conditions and transitions. If a nested rule does not activate, debugging output helps identify which part of its combined condition failed.
As was mentioned earlier, sequence numbers are assigned to expanded rules, so if you have many of nested rules, it might be a good idea to assign names to them for easier debugging.
Missing targets
When
DisplayStateEngine
debugging includes
"missing_targets",
you will see warnings if a rule references a target that does not
exist in the component template. For example:
["#shippng"] // typo
Instead of silently failing, the engine reports the issue clearly.
Recommended workflow
When something behaves unexpectedly:
- Enable debug with specific categories.
- Trigger the relevant field or flag change.
- Observe which states matched and which transitions ran.
- Adjust rules as needed.
- Disable debugging once the issue is resolved.
State rules are deterministic — bebugging simply reveals the engine's decision process so you can verify that your declarative logic matches your intent.
In the next section we will look at advanced usage topics such as
overlay,
eligible, and the difference between
switch()
and
requestSwitch()