ControlFlowException as Falsy Wrapper
See also: Invariants · Concepts
Decision
action() catches all exceptions from its target and wraps them in ControlFlowException, whose __bool__() returns False. The exception is stored in .exception and never re-raised at the node boundary.
Context
A behavior tree node can fail in two ways: expected failure (a condition evaluated false) and unexpected failure (an exception). Both must produce a falsy value so composite nodes (sequence, fallback) can treat them uniformly with if result:.
Three alternatives exist:
- Re-raise exceptions — composite nodes must
try/excepton every child call; error handling leaks into tree logic - Return
Falseon exception — original cause is lost, debugging impossible - Wrap in a falsy exception object — falsy like
False, but carries the original cause
Why This Approach
Composite nodes stay clean. sequence, fallback, and parallele test if result: with no try/except. They do not need to distinguish exception-failure from condition-failure.
The cause is never lost. .exception holds the original exception. Callers that care about the cause can inspect it; callers that don't can ignore it.
ControlFlowException.instantiate() prevents double-wrapping. A ControlFlowException passed to instantiate() is returned unchanged, so wrapping at multiple levels is safe.
Trade-offs
- Callers that re-raise must unwrap:
raise e.exceptionnotraise e - An unhandled
ControlFlowExceptionat the top level is falsy — it will not triggerexcept Exceptionhandlers that test truthiness, though it will be caught byexcept Exceptionnormally