Error Handling
The Go AI SDK provides robust error handling with specific error types for different failure scenarios. All errors follow Go's standard error handling patterns.
Handling Regular Errors
Regular errors are returned from functions and should be checked using Go's idiomatic error handling:
import (
"context"
"log"
"github.com/digitallysavvy/go-ai/pkg/ai"
"github.com/digitallysavvy/go-ai/pkg/providers/openai"
)
func main() {
ctx := context.Background()
provider := openai.New(openai.Config{APIKey: os.Getenv("OPENAI_API_KEY")})
model, _ := provider.LanguageModel("gpt-4")
result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: "Write a vegetarian lasagna recipe for 4 people.",
})
if err != nil {
// Handle error
log.Printf("Error generating text: %v", err)
return
}
fmt.Println(result.Text)
}
See Error Types below for more information on the different types of errors that may be returned.
Handling Streaming Errors
When errors occur during streaming, you should check for errors both while reading from the stream channel and after the stream completes.
Simple Text Streaming
import (
"context"
"fmt"
"log"
"github.com/digitallysavvy/go-ai/pkg/ai"
)
func main() {
ctx := context.Background()
stream, err := ai.StreamText(ctx, ai.StreamTextOptions{
Model: model,
Prompt: "Write a vegetarian lasagna recipe for 4 people.",
})
if err != nil {
log.Fatal(err)
}
// Read from stream
for chunk := range stream.Chunks() {
fmt.Print(chunk.Text)
}
// Check for errors after stream completes
if err := stream.Err(); err != nil {
log.Printf("Stream error: %v", err)
return
}
}
Full Stream with Error Chunks
Full streams support error chunks within the stream itself. You should handle both chunk errors and post-stream errors:
import (
"context"
"fmt"
"log"
"github.com/digitallysavvy/go-ai/pkg/ai"
"github.com/digitallysavvy/go-ai/pkg/provider"
)
func main() {
ctx := context.Background()
stream, err := ai.StreamText(ctx, ai.StreamTextOptions{
Model: model,
Prompt: "Write a vegetarian lasagna recipe for 4 people.",
})
if err != nil {
log.Fatal(err)
}
// Read from full stream
for chunk := range stream.Chunks() {
switch chunk.Type {
case provider.ChunkTypeText:
fmt.Print(chunk.Text)
case provider.ChunkTypeToolCall:
fmt.Printf("Tool call: %s\n", chunk.ToolCall.ToolName)
case provider.ChunkTypeError:
// Handle error chunk
log.Printf("Error in stream: %v", chunk.Error)
case provider.ChunkTypeFinish:
fmt.Printf("\nFinished: %s\n", chunk.FinishReason)
}
}
// Check for errors after stream completes
if err := stream.Err(); err != nil {
log.Printf("Stream error: %v", err)
return
}
}
Handling Context Cancellation
Go uses context.Context for cancellation and timeouts. When a context is canceled, operations return a context error:
import (
"context"
"fmt"
"time"
)
func main() {
// Create context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
stream, err := ai.StreamText(ctx, ai.StreamTextOptions{
Model: model,
Prompt: "Write a very long story...",
})
if err != nil {
log.Fatal(err)
}
for chunk := range stream.Chunks() {
fmt.Print(chunk.Text)
}
// Check if operation was canceled or timed out
if err := stream.Err(); err != nil {
if ctx.Err() == context.DeadlineExceeded {
fmt.Println("\nOperation timed out")
} else if ctx.Err() == context.Canceled {
fmt.Println("\nOperation was canceled")
} else {
fmt.Printf("\nStream error: %v\n", err)
}
}
}
OnFinish Callback with Context
The OnFinish callback is only called when the operation completes normally. It is not called when the context is canceled:
stream, err := ai.StreamText(ctx, ai.StreamTextOptions{
Model: model,
Prompt: "Write a story...",
OnFinish: func(result *ai.StreamTextResult) {
// Called only on normal completion, NOT on cancellation
fmt.Printf("Completed after %d steps\n", len(result.Steps()))
fmt.Printf("Total tokens used: %d\n", result.Usage().GetTotalTokens())
},
})
// Handle context cancellation separately
for chunk := range stream.Chunks() {
fmt.Print(chunk.Text)
}
if ctx.Err() == context.Canceled {
fmt.Println("Stream was canceled")
// Perform cleanup operations here
}
Error Types
The Go AI SDK provides several specific error types for different failure scenarios. Use errors.Is() and errors.As() to check for specific error types.
Provider Error
Represents an error from an AI provider (e.g., invalid API key, model not found, rate limiting):
import (
"errors"
"fmt"
providererrors "github.com/digitallysavvy/go-ai/pkg/provider/errors"
)
result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: "Generate text",
})
if err != nil {
var providerErr *providererrors.ProviderError
if errors.As(err, &providerErr) {
fmt.Printf("Provider: %s\n", providerErr.Provider)
fmt.Printf("Status Code: %d\n", providerErr.StatusCode)
fmt.Printf("Error Code: %s\n", providerErr.ErrorCode)
fmt.Printf("Message: %s\n", providerErr.Message)
return
}
}
Rate Limit Error
Represents a rate limit error from a provider:
import (
"errors"
"time"
providererrors "github.com/digitallysavvy/go-ai/pkg/provider/errors"
)
result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: "Generate text",
})
if err != nil {
var rateLimitErr *providererrors.RateLimitError
if errors.As(err, &rateLimitErr) {
fmt.Printf("Rate limit exceeded for %s\n", rateLimitErr.Provider)
if rateLimitErr.RetryAfterSeconds != nil {
fmt.Printf("Retry after %d seconds\n", *rateLimitErr.RetryAfterSeconds)
time.Sleep(time.Duration(*rateLimitErr.RetryAfterSeconds) * time.Second)
// Retry the request...
}
return
}
}
Validation Error
Represents a validation error (e.g., invalid parameters, schema validation failure):
import (
"errors"
providererrors "github.com/digitallysavvy/go-ai/pkg/provider/errors"
)
result, err := ai.GenerateObject(ctx, ai.GenerateObjectOptions{
Model: model,
Schema: invalidSchema,
Prompt: "Generate object",
})
if err != nil {
var validationErr *providererrors.ValidationError
if errors.As(err, &validationErr) {
fmt.Printf("Validation failed on field: %s\n", validationErr.Field)
fmt.Printf("Error: %s\n", validationErr.Message)
return
}
}
Tool Execution Error
Represents an error during tool execution:
import (
"errors"
providererrors "github.com/digitallysavvy/go-ai/pkg/provider/errors"
)
result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: "Use the weather tool",
Tools: []types.Tool{weatherTool},
})
if err != nil {
var toolErr *providererrors.ToolExecutionError
if errors.As(err, &toolErr) {
fmt.Printf("Tool %s failed (call ID: %s)\n", toolErr.ToolName, toolErr.ToolCallID)
fmt.Printf("Error: %s\n", toolErr.Message)
return
}
}
Stream Error
Represents an error during streaming:
import (
"errors"
providererrors "github.com/digitallysavvy/go-ai/pkg/provider/errors"
)
stream, err := ai.StreamText(ctx, ai.StreamTextOptions{
Model: model,
Prompt: "Generate text",
})
if err != nil {
log.Fatal(err)
}
for chunk := range stream.Chunks() {
fmt.Print(chunk.Text)
}
if err := stream.Err(); err != nil {
var streamErr *providererrors.StreamError
if errors.As(err, &streamErr) {
fmt.Printf("Stream error: %s\n", streamErr.Message)
return
}
}
Standard Errors
The SDK also provides standard error variables for common cases:
import (
"errors"
providererrors "github.com/digitallysavvy/go-ai/pkg/provider/errors"
)
// Check for specific standard errors
if errors.Is(err, providererrors.ErrInvalidInput) {
fmt.Println("Invalid input parameters")
}
if errors.Is(err, providererrors.ErrModelNotFound) {
fmt.Println("Model not found")
}
if errors.Is(err, providererrors.ErrProviderNotFound) {
fmt.Println("Provider not found")
}
if errors.Is(err, providererrors.ErrToolNotFound) {
fmt.Println("Tool not found")
}
if errors.Is(err, providererrors.ErrValidationFailed) {
fmt.Println("Validation failed")
}
if errors.Is(err, providererrors.ErrUnsupportedFeature) {
fmt.Println("Feature not supported by provider")
}
Comprehensive Error Handling Example
Here's a complete example showing robust error handling:
package main
import (
"context"
"errors"
"fmt"
"log"
"os"
"time"
"github.com/digitallysavvy/go-ai/pkg/ai"
"github.com/digitallysavvy/go-ai/pkg/provider"
providererrors "github.com/digitallysavvy/go-ai/pkg/provider/errors"
"github.com/digitallysavvy/go-ai/pkg/providers/openai"
)
func generateWithRetry(ctx context.Context, model provider.LanguageModel, prompt string, maxRetries int) (*ai.GenerateTextResult, error) {
var lastErr error
for attempt := 0; attempt <= maxRetries; attempt++ {
if attempt > 0 {
fmt.Printf("Retry attempt %d/%d\n", attempt, maxRetries)
// Exponential backoff
backoff := time.Duration(1<<uint(attempt-1)) * time.Second
select {
case <-time.After(backoff):
case <-ctx.Done():
return nil, ctx.Err()
}
}
result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: prompt,
})
if err == nil {
return result, nil
}
lastErr = err
// Handle different error types
var rateLimitErr *providererrors.RateLimitError
if errors.As(err, &rateLimitErr) {
// Rate limit - respect retry-after if provided
if rateLimitErr.RetryAfterSeconds != nil {
fmt.Printf("Rate limited, waiting %d seconds...\n", *rateLimitErr.RetryAfterSeconds)
select {
case <-time.After(time.Duration(*rateLimitErr.RetryAfterSeconds) * time.Second):
continue
case <-ctx.Done():
return nil, ctx.Err()
}
}
continue
}
var providerErr *providererrors.ProviderError
if errors.As(err, &providerErr) {
// Check if error is retryable (5xx errors)
if providerErr.StatusCode >= 500 && providerErr.StatusCode < 600 {
fmt.Printf("Provider error %d, retrying...\n", providerErr.StatusCode)
continue
}
// 4xx errors are not retryable
return nil, fmt.Errorf("non-retryable provider error: %w", err)
}
// Don't retry on context errors
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return nil, err
}
// Don't retry on validation errors
if providererrors.IsValidationError(err) {
return nil, fmt.Errorf("validation error: %w", err)
}
}
return nil, fmt.Errorf("failed after %d attempts: %w", maxRetries+1, lastErr)
}
func main() {
ctx := context.Background()
provider := openai.New(openai.Config{APIKey: os.Getenv("OPENAI_API_KEY")})
model, err := provider.LanguageModel("gpt-4")
if err != nil {
log.Fatal(err)
}
result, err := generateWithRetry(ctx, model, "Explain quantum computing", 3)
if err != nil {
log.Fatalf("Failed to generate text: %v", err)
}
fmt.Println(result.Text)
}
Best Practices
1. Always Check Errors
Never ignore errors - always check and handle them appropriately:
// Good
result, err := ai.GenerateText(ctx, options)
if err != nil {
return fmt.Errorf("failed to generate text: %w", err)
}
// Bad
result, _ := ai.GenerateText(ctx, options)
2. Use Error Wrapping
Wrap errors to provide context:
result, err := ai.GenerateText(ctx, options)
if err != nil {
return fmt.Errorf("failed to process user request: %w", err)
}
3. Check Specific Error Types
Use errors.As() and errors.Is() to check for specific errors:
var rateLimitErr *providererrors.RateLimitError
if errors.As(err, &rateLimitErr) {
// Handle rate limit specifically
}
4. Handle Context Cancellation
Always respect context cancellation:
for chunk := range stream.Chunks() {
select {
case <-ctx.Done():
return ctx.Err()
default:
fmt.Print(chunk.Text)
}
}
5. Use Defer for Cleanup
Use defer to ensure cleanup happens even on error:
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel() // Always called, even on error
result, err := ai.GenerateText(ctx, options)
// ...
6. Log Errors Appropriately
Log errors with sufficient context for debugging:
if err != nil {
log.Printf("Failed to generate text for user %s: %v", userID, err)
return err
}
7. Don't Retry Non-Retryable Errors
Some errors shouldn't be retried (validation errors, 4xx errors):
if providererrors.IsValidationError(err) {
return err // Don't retry
}
var providerErr *providererrors.ProviderError
if errors.As(err, &providerErr) && providerErr.StatusCode >= 400 && providerErr.StatusCode < 500 {
return err // Don't retry 4xx errors
}
8. Implement Exponential Backoff
When retrying, use exponential backoff to avoid overwhelming the service:
for attempt := 0; attempt <= maxRetries; attempt++ {
if attempt > 0 {
backoff := time.Duration(1<<uint(attempt-1)) * time.Second
time.Sleep(backoff)
}
// Attempt operation...
}
9. Check Stream Errors After Completion
Always check for errors after a stream completes:
for chunk := range stream.Chunks() {
fmt.Print(chunk.Text)
}
if err := stream.Err(); err != nil {
return fmt.Errorf("stream failed: %w", err)
}
10. Use OnFinish for Success Cases Only
Remember that OnFinish is only called on successful completion:
stream, _ := ai.StreamText(ctx, ai.StreamTextOptions{
Model: model,
Prompt: "...",
OnFinish: func(result *ai.StreamTextResult) {
// Only called on success
saveToDatabase(result)
},
})
// Handle cancellation/errors separately
if ctx.Err() == context.Canceled {
cleanupPartialResults()
}