Skip to content

Error Handling Guide

This document describes the error types and error handling patterns used throughout tooldiscovery.

Error Types by Package

index Package

Error When Returned Example Cause
ErrNotFound Tool/backend lookup fails GetTool("nonexistent:tool")
ErrInvalidTool Tool validation fails Empty name, invalid schema
ErrInvalidBackend Backend validation fails MCP backend missing ServerName
ErrInvalidCursor Pagination cursor invalid Malformed or expired cursor
ErrNonDeterministicSearcher SearchPage with non-deterministic searcher Custom searcher without stable ordering

search Package

The search package returns errors from the underlying Bleve index but doesn't define custom error types. Errors are typically related to index operations.

semantic Package

Error When Returned Example Cause
ErrInvalidSearcher Searcher missing components NewSearcher(nil, nil)
ErrInvalidDocumentID Document ID is empty idx.Add(ctx, Document{})
ErrInvalidEmbedder Embedder is nil NewEmbeddingStrategy(nil)
ErrInvalidHybridConfig Invalid hybrid config Alpha outside [0,1] range

tooldoc Package

Error When Returned Example Cause
ErrNotFound Tool not in index/resolver Unknown tool ID
ErrInvalidDetail Invalid detail level Unrecognized DetailLevel value
ErrNoTool No tool source configured Store without Index or ToolResolver
ErrArgsTooLarge Example args exceed limits Nesting > 5 or keys > 50

discovery Package

Error When Returned Example Cause
ErrNotFound Tool lookup fails Forwarded from index package

Error Checking Patterns

Using errors.Is

Always use errors.Is for error checking, as errors may be wrapped:

tool, backend, err := idx.GetTool("github:create-issue")
if errors.Is(err, index.ErrNotFound) {
    // Tool doesn't exist
    log.Printf("Tool not found: %s", toolID)
    return
}
if err != nil {
    // Other error
    return fmt.Errorf("failed to get tool: %w", err)
}

Handling Validation Errors

Validation errors often wrap the sentinel error with additional context:

err := idx.RegisterTool(tool, backend)
if errors.Is(err, index.ErrInvalidTool) {
    // Tool validation failed - check the error message for details
    log.Printf("Invalid tool: %v", err)
    return
}
if errors.Is(err, index.ErrInvalidBackend) {
    // Backend validation failed
    log.Printf("Invalid backend: %v", err)
    return
}

Handling Search Errors

results, err := searcher.Search(ctx, query)
if errors.Is(err, semantic.ErrInvalidSearcher) {
    // Searcher not properly configured
    log.Fatal("Searcher missing index or strategy")
}
if errors.Is(err, semantic.ErrInvalidEmbedder) {
    // Embedder is nil (for embedding/hybrid strategies)
    log.Fatal("Embedder required for semantic search")
}
if err != nil {
    // May be context cancellation or embedder error
    return fmt.Errorf("search failed: %w", err)
}

Handling Documentation Errors

doc, err := store.DescribeTool(toolID, tooldoc.DetailFull)
if errors.Is(err, tooldoc.ErrNotFound) {
    // Tool not found in index
    return nil, fmt.Errorf("unknown tool: %s", toolID)
}
if errors.Is(err, tooldoc.ErrNoTool) {
    // Store has no way to look up tools
    log.Fatal("Store not configured with Index or ToolResolver")
}
if errors.Is(err, tooldoc.ErrInvalidDetail) {
    // Invalid detail level (shouldn't happen with constants)
    return nil, fmt.Errorf("invalid detail level")
}

Context Errors

Many operations accept a context and honor cancellation:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

results, err := searcher.Search(ctx, query)
if errors.Is(err, context.DeadlineExceeded) {
    log.Printf("Search timed out")
    return nil, err
}
if errors.Is(err, context.Canceled) {
    log.Printf("Search was canceled")
    return nil, err
}

Wrapping Errors

When propagating errors, wrap them with context:

func (s *MyService) FindTools(query string) ([]Tool, error) {
    results, err := s.discovery.Search(ctx, query, 10)
    if err != nil {
        return nil, fmt.Errorf("tool search failed: %w", err)
    }

    var tools []Tool
    for _, r := range results {
        tool, _, err := s.discovery.GetTool(r.Summary.ID)
        if err != nil {
            // Log but continue - tool may have been removed
            log.Printf("Failed to get tool %s: %v", r.Summary.ID, err)
            continue
        }
        tools = append(tools, tool)
    }
    return tools, nil
}

Validation Before Operations

Validate inputs before expensive operations:

// Validate tool before registration
if tool.Name == "" {
    return fmt.Errorf("tool name is required")
}
if tool.InputSchema == nil {
    return fmt.Errorf("tool InputSchema is required")
}

// Now safe to register
if err := idx.RegisterTool(tool, backend); err != nil {
    return fmt.Errorf("registration failed: %w", err)
}

Error Recovery Patterns

Graceful Degradation

func (s *MyService) Search(query string) ([]Result, error) {
    // Try hybrid search first
    if s.embedder != nil {
        results, err := s.hybridSearch(query)
        if err == nil {
            return results, nil
        }
        // Log and fall back to BM25
        log.Printf("Hybrid search failed, falling back to BM25: %v", err)
    }

    // Fall back to BM25-only search
    return s.bm25Search(query)
}

Retry with Backoff

func (e *MyEmbedder) EmbedWithRetry(ctx context.Context, text string) ([]float32, error) {
    var lastErr error
    for i := 0; i < 3; i++ {
        vec, err := e.Embed(ctx, text)
        if err == nil {
            return vec, nil
        }
        lastErr = err

        // Don't retry on context errors
        if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
            return nil, err
        }

        // Exponential backoff
        time.Sleep(time.Duration(1<<i) * 100 * time.Millisecond)
    }
    return nil, fmt.Errorf("embedding failed after 3 retries: %w", lastErr)
}

Testing Error Conditions

func TestGetTool_NotFound(t *testing.T) {
    idx := index.NewInMemoryIndex()

    _, _, err := idx.GetTool("nonexistent:tool")

    if !errors.Is(err, index.ErrNotFound) {
        t.Errorf("expected ErrNotFound, got %v", err)
    }
}

func TestRegisterTool_InvalidTool(t *testing.T) {
    idx := index.NewInMemoryIndex()

    // Tool with empty name
    tool := model.Tool{
        Tool: mcp.Tool{
            Name: "", // Invalid
            InputSchema: map[string]any{"type": "object"},
        },
    }

    err := idx.RegisterTool(tool, model.NewMCPBackend("server"))

    if !errors.Is(err, index.ErrInvalidTool) {
        t.Errorf("expected ErrInvalidTool, got %v", err)
    }
}