Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Debugging with Structured Error Information

Introduction

In our last lesson, we focused on implementing timeouts, giving us a crucial mechanism to prevent agent tasks from running indefinitely. This ensures your system remains responsive. Now that we can reliably catch failures like timeouts, retries, and other exceptions, the next logical step is to ensure these events are not silent. Simply catching an error isn't enough for a production system; we need to understand what failed, why, and in what context.

This lesson will guide you through the process of surfacing rich, structured error information. We'll move beyond simple string-based exceptions to a more robust pattern that makes debugging, logging, and monitoring your agentic infrastructure vastly more effective. You will learn how to design and implement a system where errors are treated as first-class, data-rich objects, a practice essential for any senior developer building scalable services.

The Problem with Unstructured Errors

As an experienced developer, you know that an error log filled with messages like "An error occurred" or just a raw stack trace is of limited use, especially in a distributed system. To effectively diagnose issues in your complex agentic architecture, you need logs that you can easily query, filter, and aggregate. This requires structure.

Let's watch a short segment that perfectly illustrates the difference between basic and structured logging, and the best practices for what information to include.

12 Logging BEST Practices in 12 minutes

The video "12 Logging BEST Practices in 12 minutes" from the Better Stack channel provides an excellent overview of modern logging standards.

Please watch these two key sections: Structured Logging: Pay attention to the contrast between a simple text log and a structured JSON log. Note the benefits for filtering and analysis. Context is King: This section details the kind of information that makes a log entry truly useful. As you watch, think about how these concepts map to your agentic system (e.g., userID -> tenantID, requestID -> correlationID, etc.).

The principles from the video are directly applicable to our goal. A structured error in our system should be a JSON object containing:

  • A unique errorType (e.g., TOOL_EXECUTION_FAILED, MODEL_VALIDATION_ERROR).
  • A message for human readability.
  • A context object with details like agentId, toolName, tenantId, and a requestId to trace the operation through the system.
  • The original cause of the error, such as a stack trace or the response from a failed API call.

A Type-Safe Pattern for Handling Errors: The Result Type

Instead of relying on try...catch blocks for control flow, we can adopt a more explicit and type-safe pattern popular in functional programming and robust systems design: the Result pattern.

The idea is simple: a function never throws an exception. Instead, it always returns a "result" object that represents either a success or a failure. This forces the calling code to consciously handle both possibilities, often with compile-time guarantees from TypeScript.

This approach elegantly separates the happy path from error-handling logic and makes the error information itself a structured part of your function's return signature. The following video provides a masterful walkthrough of this concept, from first principles to a production-ready library.

How To Handle Errors Like A Senior Dev

The video "How To Handle Errors Like A Senior Dev" from Web Dev Simplified is an outstanding resource that demonstrates the evolution from basic error handling to this advanced, type-safe pattern.

Please watch the following segments: Initial Problem: This section clearly articulates the issues with code duplication and tightly-coupled error handling that arise from basic try/catch logic, especially when serving multiple clients (e.g., a web UI and an API). The Result Pattern: This is the core of the lesson. The presenter builds a simple, effective result type from scratch using TypeScript generics and discriminated unions (reason). This is a powerful technique. Consuming the Result: Here, you'll see the payoff. The consuming code can use a switch statement with TypeScript's type narrowing to handle each specific error reason. The use of satisfies never to ensure all error cases are handled is a fantastic pattern for production code.

The final pattern, where a function returns a tuple [error, data], and the error object has a reason property, provides a clean, self-documenting, and fully type-safe way to manage failures.

Applying Structured Errors in Your Agentic System

Now let's integrate these concepts into our agentic architecture.

1. Tool Execution Results

Your custom tools are a primary source of potential failures. A tool shouldn't just throw when an external API call fails. Instead, it should return a structured result.

// Define your structured error types
type ToolError = 
  | { type: 'API_ERROR'; statusCode: number; message: string; }
  | { type: 'TIMEOUT'; message: string; }
  | { type: 'INVALID_INPUT'; message: string; details: any; };

// Define the Result type
type Result<TData, TError> = { success: true; data: TData } | { success: false; error: TError };

// Example tool implementation
async function queryDatabaseTool(params: { query: string }): Promise<Result<any[], ToolError>> {
  try {
    // ... logic to execute query ...
    const results = await db.query(params.query);
    return { success: true, data: results };
  } catch (e) {
    if (e.code === 'DB_TIMEOUT') {
      return { success: false, error: { type: 'TIMEOUT', message: 'Database query timed out.' } };
    }
    // Return a different structured error for other database issues
    return { success: false, error: { type: 'API_ERROR', statusCode: 500, message: 'Internal database error.' } };
  }
}

This approach is echoed in professional AI engineering workflows. The practice of having validation steps produce structured error outputs is a cornerstone of building reliable agentic systems.

How to Build a Structured AI Coding Workflow with Deterministic and ...

This article from MindStudio, while focused on a code-generation workflow, presents principles that are universally applicable to agentic systems. It demonstrates how deterministic validation nodes are designed to produce structured data about failures.

Focus on these two points in the article: Under "Step 2: Validate", read the final paragraph: "Each validation node should produce structured output". This reinforces our approach of returning a structured object ({ pass/fail, error messages, etc. }) instead of just throwing an error. Under "Fix Prompt Design", observe how the prompt is fed with cleanly formatted, separate sections for different error types: LINT ERRORS, TYPE ERRORS, etc. This shows the downstream benefit of having structured error information—it can be used programmatically, even as input for another AI.

2. Handling Vercel AI SDK-Specific Errors

The Vercel AI SDK itself provides structured errors for certain conditions. A key example is when you request structured output from an LLM and it fails to produce a valid object. Your system should catch this specific error and map it to your application's standardized error format.

Generating Structured Data

The official AI SDK documentation details the specific errors thrown during structured data generation.

Please review the section titled Error Handling. Pay close attention to the AI_NoObjectGeneratedError and the properties it contains, like cause, text, and usage.

Here is how you can handle this error within your agent logic and wrap it in our Result type:

import { generateText, Output, NoObjectGeneratedError } from 'ai';
import { z } from 'zod';

// Assuming your own standardized error format
type AppError = {
  type: 'MODEL_OUTPUT_INVALID';
  message: string;
  cause?: any;
  details: {
    rawText?: string;
    modelUsage?: { promptTokens: number; completionTokens: number };
  }
};

async function getStructuredData(prompt: string): Promise<Result<any, AppError>> {
  const schema = z.object({ name: z.string() });

  try {
    const { object } = await generateText({
      model,
      output: Output.object({ schema }),
      prompt,
    });
    return { success: true, data: object };
  } catch (error) {
    if (NoObjectGeneratedError.isInstance(error)) {
      console.error('Model failed to generate a valid object:', error);
      // Map the SDK error to your application's standard error format
      return {
        success: false,
        error: {
          type: 'MODEL_OUTPUT_INVALID',
          message: 'The model failed to generate a valid structured object.',
          cause: error.cause,
          details: {
            rawText: error.text,
            modelUsage: {
                promptTokens: error.usage.promptTokens,
                completionTokens: error.usage.completionTokens
            },
          },
        },
      };
    } else {
      // Handle other unexpected errors
      throw error; // Or wrap in another standard error type
    }
  }
}

By catching the specific NoObjectGeneratedError and transforming it, you create a consistent error handling strategy across your application, regardless of whether the error originates from your own code, a database, or the AI model provider.

Finally, for a production system, you don't need to build the Result type functionality from scratch. There are excellent, lightweight libraries that provide this. The neverthrow library, mentioned in the "How To Handle Errors Like A Senior Dev" video (LINK in this section), is a popular and robust choice in the TypeScript ecosystem.

Conclusion

In this lesson, you've learned how to elevate your system's resilience by treating errors as structured, data-rich information. This is a fundamental practice for building observable, maintainable, and production-ready agentic systems.

Key Takeaways:

  • Structured logs are essential for effective debugging and monitoring in a production environment. They should contain not just a message, but also an error type and relevant context (e.g., agentId, tenantId).
  • The Result pattern provides a type-safe and explicit way to handle errors without using exceptions for control flow. Functions return either a success object with data or a failure object with a structured error.
  • Your custom tools should adopt this pattern, returning a structured Result object to clearly communicate success or failure to the calling agent.
  • SDK-specific errors, like the Vercel AI SDK's NoObjectGeneratedError, should be caught and mapped to your application's standardized error format for consistent handling.

In our next lesson, we will build upon this foundation. Now that we can produce and log structured information about failures, we can implement a circuit breaker pattern. This will allow our system to automatically detect when an external service is consistently failing and temporarily stop sending requests to it, preventing cascading failures and improving overall system stability.

Can't find a good explanation? Sign up and we'll make it for you

Sign up