Skip to content

Exception System ​

AmritaSense defines a lean, purpose-specific exception hierarchy for handling various errors and interruptions during workflow execution. Each exception type has clear semantic boundaries and usage constraints.

InterruptNotice ​

python
class InterruptNotice(BaseException):
    """Special exception for immediate workflow termination.

    Raised by the RESET node or external systems. As a BaseException
    subclass, it bypasses regular CATCH blocks and penetrates directly to
    the interpreter's top-level handler, ensuring clean and unconditional
    termination.
    """

Why inherit from BaseException

Python's except Exception does not catch BaseException subclasses. Therefore, no Try/CATCH block in the workflow can intercept InterruptNotice by default. This is an intentional design choice — RESET must be an "uncatchable" emergency termination signal. The only exception is explicitly adding InterruptNotice to exception_ignored, at which point it becomes catchable as a regular exception.

Trigger methods

  • Composition level: Automatically raised when execution reaches an RESET node.
  • External injection: External systems can directly raise InterruptNotice(); the interpreter catches and terminates at the next node boundary.

Interpreter response

When the interpreter main loop catches InterruptNotice:

  1. Logs the current pointer position and notification message
  2. Clears _ret_addr_stack (the call stack)
  3. Resets _pointer (the pointer vector)
  4. Resets the entire status register
  5. The workflow exits cleanly with no residual state

InterruptKeepContext ​

InterruptKeepContext is a subclass of InterruptNotice that terminates workflow execution while preserving interpreter state. Unlike InterruptNotice, which triggers a full reset(), this exception leaves the pointer, call stack, and dependency injection parameters intact.

Use cases:

  • Pause-inspect-resume debugging
  • Checkpoint-restart workflows
  • Triggered by the SUSPEND instruction node

NullPointerException ​

python
class NullPointerException(Exception):
    """Raised when a node cannot be located at a given address.

    This occurs when jump operations reference non-existent nodes,
    invalid address vectors, or aliases that failed to resolve.
    """

Trigger scenarios

  • A JMP, INVOKE, or other jump instruction's target address does not exist in the NodeComposeRendered.
  • An alias lookup fails in alias2vector_map (at which point JumpNode and InvokeNode's _post_compile raises AliasNotFoundError).
  • An out-of-bounds index is accessed at runtime via find_addr.

Relationship with alias validation

NullPointerException is the fallback exception for runtime address failures. In practice, when using ALIAS + JMP/INVOKE for normal addressing, typos are caught at compile time during _post_compile with correction suggestions. Only raw list[int] addresses that are invalid at runtime will raise this exception.

BreakLoop ​

python
class BreakLoop(Exception):
    """Exception used to break out of WHILE or DO-WHILE loop constructs.

    When raised within a loop body, the loop terminates immediately and
    execution continues after the loop's NOP exit point.
    """

Automatic penetration mechanism

BreakLoop is automatically added to the _exc_ignored tuple during WorkflowInterpreter initialization. This means:

  • Any Try/CATCH block inside a loop body cannot catch BreakLoop.
  • It penetrates all intermediate exception handling layers and reaches the innermost WhileNode or DONode.
  • The loop node catches BreakLoop and executes jump_near(NOP), exiting cleanly.

This auto-inclusion can be disabled via __flags__.DISABLE_EXC_IGNORED = True from amrita_sense._unsafe. See Unsafe Features for details.

Usage

Simply raise it inside the ACTION or DO node of a loop body:

python
@Node()
def process_item():
    if item is None:
        raise BreakLoop  # No more data; exit the loop
    if item.should_skip:
        return  # Equivalent to continue
    handle(item)

Note: Developers should not manually add BreakLoop to exception_ignored — it is already added automatically during interpreter initialization. Adding it again has no additional effect; attempting to remove it would cause CATCH blocks inside loops to accidentally capture BreakLoop, breaking loop semantics.

IllegalState ​

Raised when an operation is attempted in an invalid state. Common triggers:

  • Calling terminate_all() or wait_all() on a non-top-level interpreter
  • Starting run() on an interpreter that is already running
  • Accessing wait on an interpreter that is not running
  • Violating Try/CATCH syntax constraints (e.g., adding CATCH after FINALLY, duplicate THEN)

See Subgraph Isolation for correct usage patterns.

DependsException and subclasses ​

All dependency injection exceptions inherit from DependsException.

python
class DependsException(Exception):
    """Base class for all dependency injection related exceptions."""

AliasNotFoundError ​

Raised when a JMP or INVOKE instruction references an alias that does not exist in the workflow graph's alias registry. Detected at compile time during _post_compile. Replaces the generic RuntimeError / ValueError previously used for alias resolution failures.

DependsResolveFailed ​

python
class DependsResolveFailed(Exception):
    """Raised when a node's dependencies cannot be resolved.

    This occurs when required parameters in the function signature
    cannot be matched to any available dependency source.
    """

Trigger conditions

  • A node's function signature has an unmatched parameter (no dependency source of matching type, and no default value).
  • Multiple dependency sources match the same parameter and cannot be disambiguated.

DependsInjectFailed ​

python
class DependsInjectFailed(Exception):
    """Raised when runtime dependency injection fails during node execution.

    This typically occurs when a Depends factory function raises an
    exception that is not in the exception_ignored tuple.
    """

Trigger conditions

  • A Depends factory function raises an exception at runtime that is not in _exc_ignored.
  • When resolving multiple dependencies concurrently, all failures are collected into an ExceptionGroup and re-raised.
  • A generator dependency is resolved at a site where the scope it declares (or the default scope) is not active.

Critical behavior: Depends returning None terminates immediately

Unlike the event system's "return None to skip" behavior, in node execution, if a dependency factory declared via Depends returns None, the workflow raises an exception and terminates immediately. Nodes are atomic execution units, and dependency resolution failure means the node cannot run — this is not a "skip" scenario. Therefore, dependency factory functions designed for nodes should always return a valid value (or raise an explicit exception when unable to provide one, rather than returning None).

A generator dependency that yields None counts as a failure under the same rule, but its teardown still runs.

DependsDeclarationError ​

python
class DependsDeclarationError(Exception):
    """Raised when a dependency declaration is contradictory.

    Raised while the dependency graph is built (i.e. when a node or an
    event handler is constructed), so a malformed declaration fails fast
    instead of surfacing as a resolution failure at execution time.
    """

Trigger conditions

  • Depends appears in both the annotation and the default value of the same parameter.
  • Several Depends markers appear in the same Annotated annotation.
  • A scope is declared for a provider that is not a generator.

GraphBuildError ​

Raised when workflow graph building or rendering fails. Common triggers:

  • Duplicate alias names in the same composition
  • Attempting to build an already-built NodeComposeRendered
  • Missing original graph during rendering

StreamStateError ​

Raised when a SuspendObjectStream operation is attempted in an invalid state. Common triggers:

  • Pushing to a closed queue
  • Calling wait_to_suspend() when already waiting
  • Setting a callback function twice
  • Calling get_response_generator() when already being consumed

Exception Hierarchy ​

text
BaseException
├── InterruptNotice          # Inherits BaseException; uncatchable by CATCH by default
│   └── InterruptKeepContext  # Interrupts but preserves interpreter context

Exception
├── IllegalState              # Invalid state operation
├── NullPointerException      # Invalid address
├── BreakLoop                 # Loop exit signal
├── AliasNotFoundError        # Alias resolution failure
├── GraphBuildError           # Graph build/render failure
├── StreamStateError          # Stream operation in illegal state
└── DependsException          # Dependency injection base class
    ├── DependsResolveFailed   # Dependencies cannot be resolved
    ├── DependsInjectFailed    # Exception during dependency injection
    └── DependsDeclarationError # Contradictory dependency declaration

Design principles:

  • InterruptNotice inherits from BaseException, achieving natural uncatchability.
  • InterruptKeepContext inherits from InterruptNotice, preserving context for later recovery.
  • BreakLoop inherits from Exception, but gains equivalent penetration capability through automatic inclusion in _exc_ignored.
  • IllegalState protects interpreter tree API call validity and instruction syntax constraints.
  • AliasNotFoundError / GraphBuildError / StreamStateError are fine-grained exception types.
  • All dependency-related exceptions inherit from DependsException, allowing users to catch the entire dependency error category as needed.

search_exceptions() ​

python
from amrita_sense.utils import search_exceptions

def search_exceptions(
    seq: Sequence[BaseException | list | None],
) -> list[BaseException]

Recursively searches a sequence (potentially containing nested lists of exceptions) and returns a flat list of all BaseException instances. Used internally by FUN_BLOCK to collect exceptions from sub-interpreter trees.

Apache 2.0 License