Files
flowgraph/docs/architecture/error-handling.md
glm-5.1 eaeba38e71 resolve architecture review round 2: criticals, warnings, suggestions
- C-05: Add flowgraph-api.md with complete public API surface
- C-06: Document <Map> component in workflow-templates.md
- C-07: Specify Conditional else-branch behavior
- C-08: Add lifecycle/ownership section to reactive-execution.md
- C-09: Add consumer-integration.md end-to-end walkthrough
- W-02: Add reactive error boundary semantics (3 levels)
- W-03: Complete ReactiveContext interface definition
- W-04: Add template composition rules (8 rules)
- W-05: Document removeChild for both HostConfigs
- W-06: Document signal/effect disposal lifecycle
- W-07: Add ADR-004 (no schema version field)
- W-08: Add type compatibility depth/contract to analysis.md
- W-11: Add performance characteristics section
- S-01: Getting Started merged into consumer-integration.md
- S-02: Add flow diagrams for template rendering pipeline
- S-03: Add node status state machine diagram
- S-04: Add testing strategy section
- S-06: Validate source structure cross-references

Review round 2 fixes:
- Define TemplateNodeAttrs as alias for OperationNodeAttrs
- Document CallEventMapValue and CallResult types in schema.md
- Standardize CycleError naming (replace CircularDependencyError)
- Add function form to Map.over type definition
- Define Map aggregate completion/failure semantics
- Fix immutability claim for fromCallEvents
- Clarify edgeType storage alongside OperationEdgeAttrs
- Clarify WorkflowNode.status === statusMap (same Signal)
- Add component-to-tag mapping for WorkflowTag
2026-05-19 13:05:35 +00:00

9.8 KiB

status, last_updated
status last_updated
draft 2026-05-20

Error Handling

FlowgraphError hierarchy, validation error collection, and error boundaries.

Design Principle

Flowgraph follows taskgraph's error handling pattern:

  1. Programmer errors throw — invalid arguments, duplicate node IDs, cycles where acyclicity is enforced
  2. Operational conditions return structured results — validation errors, type mismatches, unreachable nodes
  3. Graph mutations throw on constraint violations — adding a duplicate node throws DuplicateNodeError, adding a cycle-creating edge throws CycleError

This means validation functions (validateGraph(), validateSchema(), validateTemplate()) never throw — they collect issues and return them. But construction functions (fromSpecs(), addNode(), addEdge()) do throw on constraint violations.

Error Hierarchy

FlowgraphError                    # Base class for all flowgraph errors
├── ConstructionError              # Errors during graph construction
│   ├── DuplicateNodeError         # Duplicate node key
│   ├── DuplicateEdgeError         # Duplicate edge key
│   ├── NodeNotFoundError          # Referenced node doesn't exist
│   └── CycleError                 # Adding an edge would create a cycle
├── ValidationError                # Schema validation failed (single field)
├── GraphValidationError           # Graph-level validation issue
│   ├── CycleValidationError       # Cycle detected in the graph
│   └── DanglingReferenceError     # Edge references non-existent node
├── TypeIncompatError              # Type compatibility check failed
└── InvalidTransitionError         # Invalid call status transition

FlowgraphError

class FlowgraphError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "FlowgraphError";
  }
}

Base class. All flowgraph errors inherit from this.

ConstructionError

class ConstructionError extends FlowgraphError {
  constructor(message: string) {
    super(message);
    this.name = "ConstructionError";
  }
}

Base class for errors that occur during graph construction (fromSpecs(), addNode(), addEdge(), etc.).

DuplicateNodeError

class DuplicateNodeError extends ConstructionError {
  constructor(public readonly key: string) {
    super(`Node with key "${key}" already exists`);
    this.name = "DuplicateNodeError";
  }
}

Thrown when adding a node with a key that already exists in the graph.

DuplicateEdgeError

class DuplicateEdgeError extends ConstructionError {
  constructor(
    public readonly source: string,
    public readonly target: string,
  ) {
    super(`Edge "${source} -> ${target}" already exists`);
    this.name = "DuplicateEdgeError";
  }
}

Thrown when adding an edge between two nodes that already have an edge between them.

NodeNotFoundError

class NodeNotFoundError extends ConstructionError {
  constructor(public readonly key: string) {
    super(`Node "${key}" not found in graph`);
    this.name = "NodeNotFoundError";
  }
}

Thrown when referencing a node that doesn't exist (e.g., adding an edge with a non-existent endpoint).

CycleError

class CycleError extends ConstructionError {
  constructor(public readonly cycles: string[][]) {
    super(`Adding this edge would create a cycle: ${JSON.stringify(cycles)}`);
    this.name = "CycleError";
  }
}

Thrown when adding an edge would create a cycle. The cycles field contains the cycle paths that would be created.

Note: CycleError is flowgraph's cycle error, thrown by addEdge() during construction. Taskgraph uses a different error name (CircularDependencyError, thrown by topologicalOrder()). The two are distinct errors for distinct contexts — flowgraph prevents cycles at construction time, taskgraph allows cycles and detects them later.

ValidationError

interface ValidationError {
  type: "schema";
  nodeKey: string;
  field: string;
  message: string;
  value?: unknown;
}

Returned by validateSchema() when a node's attributes don't match the TypeBox schema. This is a structured result, not a thrown error.

GraphValidationError

interface GraphValidationError {
  type: "graph";
  category: "cycle" | "dangling-reference" | "orphan-node" | "status-inconsistency";
  details: unknown;
}

Returned by validateGraph() for graph-level issues:

Category Meaning Details
cycle The graph contains cycles { cycles: string[][] }
dangling-reference An edge references a non-existent node { source: string, target: string }
orphan-node A node has no incoming or outgoing edges { nodeKey: string }
status-inconsistency A call node has incompatible status with its parent (e.g., parent completed but child still running) { nodeKey: string, parentKey: string, nodeStatus: string, parentStatus: string }

InvalidTransitionError

class InvalidTransitionError extends FlowgraphError {
  constructor(
    public readonly requestId: string,
    public readonly from: CallStatus,
    public readonly to: CallStatus,
  ) {
    super(`Invalid status transition for call ${requestId}: ${from}${to}`);
    this.name = "InvalidTransitionError";
  }
}

Thrown when updateNodeStatus() is called with an invalid transition (e.g., completed → running).

TypeIncompatError

interface TypeIncompatError {
  type: "type-compat";
  sourceKey: string;
  targetKey: string;
  compatible: false;
  mismatches: TypeMismatch[];
}

interface TypeMismatch {
  path: string;
  expected: string;
  actual: string;
}

Returned by validateTemplate() and analyzeTypeCompat() when an edge between operations has incompatible type schemas. This is a structured result, not a thrown error.

Error Collection

Validation functions collect all issues into an array and return them. They do not throw on the first error:

const errors = graph.validate();
// errors is AnyValidationError[], which may be empty

for (const error of errors) {
  if (error.type === "schema") {
    console.log(`Node ${error.nodeKey} has invalid field ${error.field}: ${error.message}`);
  } else if (error.type === "graph" && error.category === "cycle") {
    console.log(`Graph has cycles: ${error.details.cycles}`);
  }
}

This "collect all errors" pattern allows consumers to see all issues at once, rather than fixing them one at a time.

AnyValidationError

type AnyValidationError = ValidationError | GraphValidationError | TypeIncompatError;

Union type for all validation errors. Consumers use the type discriminator to handle each category:

switch (error.type) {
  case "schema":      // ValidationError
  case "graph":       // GraphValidationError
  case "type-compat": // TypeIncompatError
}

Throwing vs. Returning

The distinction between thrown errors and returned errors:

Function Behavior Rationale
addNode(key, attrs) Throws DuplicateNodeError on duplicate key Adding a duplicate is a programmer error
addEdge(source, target) Throws NodeNotFoundError on missing endpoint Edge without endpoints is invalid
addEdge(source, target) Throws CycleError if edge creates cycle DAG invariant must be maintained
updateNodeStatus(id, status) Throws InvalidTransitionError on invalid transition State machine must be enforced
validateSchema() Returns ValidationError[] Schema issues are validations, not crashes
validateGraph() Returns GraphValidationError[] Graph issues are validations, not crashes
validateTemplate() Returns AnyValidationError[] Template issues are validations, not crashes
analyzeTypeCompat() Returns TypeCompatResult (includes mismatches) Type incompatibility is advisory, not blocking
topologicalOrder() Throws CycleError on cycles No valid ordering exists from a cyclic graph

This matches taskgraph's pattern: construction enforces invariants (throwing on violations), validation reports issues (returning error arrays).

Error Boundaries in the Call Graph

The call graph has an additional error boundary: updateFromEvent(). Call events arrive from the pub/sub layer and may reference unknown operations or have invalid transitions. The error boundary handles these gracefully:

  • Unknown requestId in a call.responded event → log warning, ignore the event (the call may have been created by a different process)
  • Invalid status transition → log warning, ignore the event (the call may have transitioned in a different order)
  • Unknown operationId → create the node anyway with status: "pending" (the operation may be registered later)

This makes updateFromEvent() resilient to out-of-order, duplicate, and partial events. Errors are logged but don't crash the process.

Constraints

  • Validation functions never throw — they collect errors and return them. This is a contract.
  • Construction functions throw on invariant violations — adding a cycle-creating edge is a programming error, not a validation finding.
  • All errors have structured dataCycleError includes cycle paths, InvalidTransitionError includes from/to status, TypeIncompatError includes mismatch details.
  • Error messages are descriptive — errors include enough context to diagnose the problem without additional lookups.
  • Error classes follow the taskgraph pattern — naming, structure, and behavior match @alkdev/taskgraph_ts/src/error/.

References

  • Taskgraph errors: @alkdev/taskgraph_ts/src/error/
  • Call protocol events: @alkdev/operations/src/call.ts
  • Schema: schema.md