Skip to content

⚠️ Error Handling

dagpipe raises a small, predictable set of exceptions. Know when they surface and how to react.


📋 Exception reference

Exception Raised when Where
SchemaError Payload violates the declared schema State.__post_init__, Schema.validate_payload, Schema.validate_update, Schema._walk, Schema._check_type, Schema._validate_path
TypeError resolve()/resolve_async() yields a non-State Node.run, AsyncNode.run_async
TypeError Engine(...) receives a non-Node element or a non-Sequence/Graph Engine.__init__
TypeError Engine.run/run_async receives a non-State root Engine.run, Engine.run_async
TypeError Graph.add_edge/add_root receives a non-Node Graph.add_edge, Graph.add_root
TypeError A YAML node class path is not a Node subclass _load_nodes
ValueError Adding an edge would create a cycle (incl. self-cycle) Graph.add_edge
ValueError Node.id is not valid dotted snake_case Node.clean_id_and_name
RuntimeError Engine mode is corrupt (should never happen) Engine.run

🧊 Schema failures

1
2
3
4
5
6
7
8
9
from dagpipe import Payload, Schema, SchemaError, State

class UserState(State):
    schema = Schema({"name": str, "zip": int | None})

try:
    UserState(payload=Payload({"name": 42, "zip": "abc"}))
except SchemaError as e:
    print(e)   # Path 'name' must be str

Guidance:

  • A SchemaError during construction means the root can never exist — fail the request/concept early.
  • SchemaError during fork(payload_update=...) means the update path was not declared — fix the schema or the update, don't swallow it.

🔁 Cycle failures

1
2
3
4
5
6
7
8
from dagpipe import Graph

g = Graph()
g.add_edge(a, b)
try:
    g.add_edge(b, a)     # would create a cycle
except ValueError as e:
    print(e)             # Adding edge b → a would create cycle

Cycles (including a → a) always raise immediately at wiring time, never at execution. Treat them as a design error: redraw the topology.


🧬 Bad node output

class BadNode(Node):
    id = "bad.output"

    def resolve(self, state):
        yield "not a state"    # ← not a State

engine = Engine([BadNode()])
try:
    engine.run(root)
except TypeError as e:
    print(e)   # bad.output.resolve must yield State, got <class 'str'>

This check is applied per yielded object, so one bad element in a generator fails the whole run.


🪜 Step-wise status, not errors

run_steps doesn't raise when a node produces no output — it reports completed=False:

1
2
3
for step in engine.run_steps(root):
    if not step.completed:
        log.info("%s produced no state (branch pruned)", step.node_id)

Treat completed=False as a signal, not an exception. See use case 07.


💡 Handling strategies

  • At the boundary: construct states/pipelines in a wrapper that converts SchemaError into a user-facing 4xx (API) or a descriptive FAILED status (batch jobs).
  • At the graph: let ValueError from add_edge propagate during setup — it's a programming error you want to see in CI.
  • Never catch State/Payload errors mid-pipeline and continue with a fallback payload — the pipeline was designed to enforce invariants.