Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 29

Robust Error Handling in Agent Workflows

Recommended resources

Hello again! I hope you had a chance to experiment with the AsyncPostgresSaver and see your workflow's state persisted in your database.

In our last lesson, we built a crucial foundation for a production-ready system: durable state persistence. Now, we can pause and resume workflows, even across application restarts. But what happens when things go wrong within a workflow? A data source API might be temporarily unavailable, or an LLM call might fail.

This brings us to our current learning outcome: Incorporate error handling and retry mechanisms into critical agent nodes. We'll build on our persistent state to create resilient nodes that can gracefully handle failures, retry when appropriate, and avoid bringing the entire workflow to a halt. This is a standard practice in building robust, event-driven systems like the ones you've engineered before.

Let's look at a conceptual model of what we're aiming for.

Workflow with Error Handling and Retry Mechanisms
This flowchart illustrates the core pattern we'll implement. A main processing node, upon encountering an issue, can trigger an auxiliary process (or logic within the node itself) that decides whether to retry the step or move on.

We will explore three powerful patterns for achieving this in LangGraph:

  1. Manual Retry Logic: Implementing custom retry counters and logic directly within our agent's state.
  2. Dynamic Interrupts: Programmatically pausing the graph from within a node when a specific condition is met, allowing for external intervention or alternative paths.
  3. Agentic Self-Critique: A more advanced pattern where one node "grades" the output of another, allowing the graph to self-correct based on quality, not just technical errors.

Pattern 1: Manual Retry Logic via State

The most direct and flexible way to handle retries in LangGraph.js is to manage the retry logic yourself, using the graph's state. This gives you full control over the conditions for retrying, the number of attempts, and what to do when all retries fail.

An article by RedPill Analytics provides an excellent, clear example of this pattern in a data workflow context. While the code is in Python, the logic is identical to what we'll implement in TypeScript.

Using LangGraph Agents to Orchestrate Data Workflows

Let's review the blog post 'Using LangGraph Agents to Orchestrate Data Workflows'. This will give us a concrete template for building our retry mechanism.

Please read the following sections: Start with the AgentState definition, noticing the dataingestion_retry fields. Read the # Execute Data Extraction Job section to see how a try...catch block is used to increment the retry counter. Read the # Verify rowcount section for a more complex example where a validation failure can also trigger a retry. Finally, look at the # Transitions section to see how the status field in the state dictates the graph's flow, including routing to a failed state. Focus on the pattern: modify state on error, and use conditional edges to react to that state change.

Implementing the Manual Retry Pattern

Let's apply this pattern to a hypothetical data_collection_node in our project. This node is a prime candidate for retries, as it might call external APIs that could fail.

1. Update the Agent State

First, we augment our AgentState interface to track retries.

// In your state definition file

interface AgentState {
  topic: string;
  article?: string;
  // ... other fields
  
  // New fields for error handling
  error?: string; // To store the last error message
  retryCount: number;
  maxRetries: number;
}

2. Modify the Node

Next, we wrap the node's core logic in a try...catch block.

// In your nodes file

async function collectDataNode(state: AgentState): Promise<Partial<AgentState>> {
  console.log("--- Collecting Data ---");
  try {
    // Simulate an API call that might fail
    const willFail = Math.random() > 0.5;
    if (willFail && state.retryCount < state.maxRetries) {
      throw new Error("Failed to fetch data from the source API.");
    }

    const collectedData = `Some fascinating data about ${state.topic}...`;
    // On success, reset retryCount and clear any previous error
    return { article: collectedData, retryCount: 0, error: undefined };
  } catch (e: any) {
    console.error(`Error in collectDataNode: ${e.message}`);
    // On failure, increment retryCount and store the error
    const newRetryCount = state.retryCount + 1;
    return {
      error: e.message,
      retryCount: newRetryCount,
    };
  }
}

3. Add a Conditional Edge

Finally, we use a conditional edge to create the retry loop. This edge checks the state after collectDataNode runs and decides where to go next.

// In your graph definition file

function shouldContinue(state: AgentState): "generateContentNode" | "collectDataNode" | "__end__" {
  if (state.error) {
    if (state.retryCount >= state.maxRetries) {
      console.log("--- Max retries reached. Ending workflow. ---");
      return "__end__";
    }
    console.log(`--- Retrying... Attempt ${state.retryCount} ---`);
    return "collectDataNode"; // Route back to the same node to retry
  }
  return "generateContentNode"; // On success, proceed to the next node
}

// ...
workflow.addNode("collectDataNode", collectDataNode);
// ... add other nodes

workflow.setEntryPoint("collectDataNode");
workflow.addConditionalEdges("collectDataNode", shouldContinue);
// ... add other edges

This setup creates a robust, self-contained retry loop. The persistence layer you built last lesson ensures that even if the application crashes during the wait between retries, it can be resumed correctly.


Pattern 2: Dynamic Interrupts for Controlled Pauses

Sometimes, a simple retry isn't enough. You might want to pause the graph and wait for external input, similar to our human-in-the-loop step, but triggered by an error. LangGraph provides a NodeInterrupt for this.

The official LangChain channel has a video that explains this concept. While it focuses on human-in-the-loop, the mechanism is what's important for us.

Human in the Loop in LangGraph.js

Let's watch a segment from the 'Human in the Loop in LangGraph.js' video. It introduces a powerful concept: dynamic interrupts, where a node can decide to pause the graph based on its internal logic.

Watch the section from 08:15 to 09:20. The key takeaway is that you can throw new NodeInterrupt() from within a node function to pause the entire graph execution. The graph can then be resumed later, just like we did with our approval step.

You could use this pattern to handle a critical failure that requires manual intervention. For instance, if a data source API returns a 401 Unauthorized error, you could throw a NodeInterrupt. The system monitoring the graph could then detect this, send a notification to an administrator to fix the API credentials, and then resume the graph.


Pattern 3: Agentic Self-Critique for Quality Control

The most advanced error handling involves making your agent "smarter." Instead of just catching technical exceptions, we can build nodes that critique the quality of the output from other nodes. This is especially relevant for our content generation pipeline.

Imagine our generateArticleNode produces an article. How do we know it's good? Is it on-brand? Is it factually correct? We can use another LLM call to act as a "critique agent."

The "Self-RAG" video you've seen before is the perfect guide to this powerful concept. It demonstrates how to build a graph where dedicated nodes grade document relevance, check for hallucinations, and evaluate answer quality.

Building a Self-RAG | Making your Agentic workflows critique through Self-Reflection | LangChain

To understand the self-critique pattern, let's revisit the 'Building a Self-RAG' video. It's a masterclass in creating agentic loops for quality control.

Please watch these key segments: Architecture Overview (07:13 - 11:30): Pay close attention to the nodes named grade_document, check_for_hallucination, and grade_answer. Notice how they fit into the overall flow. Implementing a Grader (32:06 - 36:10): This section shows the implementation of the grade_document node. The core idea is using a structured LLM call with a specific prompt to get a binary 'yes'/'no' score for relevance. This is the pattern for any critique node. Checking for Hallucination (42:44 - 46:30): This shows a more specific critique node. It checks if the generated answer is grounded in the source documents, which is a form of error/quality checking. Focus on how each critique node sets a flag in the state, which is then used by a conditional edge to control the workflow.

In our project, we could implement a critiqueNode that runs after the generateArticleNode. This node would:

  1. Take the generated article from the state.
  2. Use an LLM with a carefully crafted prompt to check for brand voice, clarity, and other quality metrics.
  3. Output a structured response, perhaps { critique: "passed" } or { critique: "failed", feedback: "The tone is too casual." }.
  4. A conditional edge would then either route to the approvalNode (if passed) or back to the generateArticleNode (if failed), possibly adding the feedback to the state to guide the rewrite.

This creates a powerful self-correction loop that goes beyond simple error handling and moves toward genuine quality assurance.


A Crucial Safety Net: The Recursion Limit

Retry loops and self-correction cycles introduce a risk: what if the agent gets stuck in an infinite loop? LangGraph provides a simple and essential safety mechanism: the recursionLimit.

You can set this in the configuration object when you invoke your graph. If the number of steps exceeds this limit, the graph will stop and throw an error.

// When invoking your graph
const config = { 
  configurable: { thread_id: "..." },
  recursionLimit: 15, // The graph will not run for more than 15 steps
};

try {
  const stream = app.stream(initialState, config);
  // ... process stream
} catch (e) {
  if (e.name === 'GraphRecursionError') {
    console.error("--- Infinite loop detected! Graph execution halted. ---");
  } else {
    throw e;
  }
}

This is your ultimate safeguard against runaway agent behavior.


Conclusion

You've now learned how to transform your workflow from a simple sequence into a robust, resilient system. By incorporating error handling, you're building an agent that can operate reliably and autonomously.

Key Takeaways:

  • Manual Retries: You can implement custom retry logic by managing retryCount and error fields in your agent's state and using conditional edges to create loops.
  • Self-Critique: You can create "grader" or "critique" nodes that use LLMs to evaluate the quality of outputs, enabling the graph to self-correct based on feedback.
  • Dynamic Interrupts: You have the ability to programmatically pause the graph from within a node using NodeInterrupt to handle situations that require external intervention.
  • Recursion Limit: You now know how to use the recursionLimit config option as a critical safety net to prevent infinite loops in your graph.

Preview of the next lesson:
We've just introduced the concept of a self-critique node. In the next lesson, we will dive deep into this pattern. The learning outcome is to "Implement LLM-based evaluation prompts to score generated content for clarity, coherence, and brand voice consistency." We will focus on the art and science of writing effective "grader" prompts to build a reliable quality gate for your content.