Context Cancellation
This guide covers common issues with Go contexts in the AI SDK, including timeouts, cancellations, and proper context handling.
Understanding Go Contexts
Go's context.Context is used for:
- Cancellation: Stop operations that are no longer needed
- Timeouts: Enforce maximum duration for operations
- Deadlines: Set absolute time limits
- Values: Pass request-scoped data (use sparingly)
Common Context Errors
Error: "Context Deadline Exceeded"
Symptoms:
Error: context deadline exceeded
Operation timed out after 30s
Cause: Operation took longer than the context timeout allows.
Solution:
package main
import (
"context"
"fmt"
"log"
"os"
"time"
"github.com/digitallysavvy/go-ai/pkg/ai"
"github.com/digitallysavvy/go-ai/pkg/providers/openai"
)
func main() {
provider := openai.New(openai.Config{
APIKey: os.Getenv("OPENAI_API_KEY"),
})
model, _ := provider.LanguageModel("gpt-4")
// PROBLEM: Timeout too short for long operations
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: "Write a comprehensive 5000-word essay on quantum physics",
})
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
log.Println("Operation timed out - need longer timeout or streaming")
// SOLUTION 1: Increase timeout
ctx2, cancel2 := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel2()
result, err = ai.GenerateText(ctx2, ai.GenerateTextOptions{
Model: model,
Prompt: "Write a comprehensive 5000-word essay on quantum physics",
})
if err != nil {
log.Printf("Still failed with longer timeout: %v", err)
// SOLUTION 2: Use streaming instead
ctx3 := context.Background()
stream, err := ai.StreamText(ctx3, ai.StreamTextOptions{
Model: model,
Prompt: "Write a comprehensive 5000-word essay on quantum physics",
})
if err != nil {
log.Fatal(err)
}
for chunk := range stream.Chunks() {
fmt.Print(chunk.Text)
}
if err := stream.Err(); err != nil {
log.Fatal(err)
}
return
}
} else {
log.Fatal(err)
}
}
fmt.Println(result.Text)
}
Error: "Context Canceled"
Symptoms:
Error: context canceled
Stream terminated early
Cause: Context was explicitly canceled before operation completed.
Solution:
package main
import (
"context"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"github.com/digitallysavvy/go-ai/pkg/ai"
"github.com/digitallysavvy/go-ai/pkg/providers/openai"
)
func main() {
provider := openai.New(openai.Config{
APIKey: os.Getenv("OPENAI_API_KEY"),
})
model, _ := provider.LanguageModel("gpt-4")
// Create cancellable context for graceful shutdown
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Handle OS signals for graceful shutdown
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
go func() {
<-sigCh
log.Println("Received interrupt signal, canceling operations...")
cancel()
}()
stream, err := ai.StreamText(ctx, ai.StreamTextOptions{
Model: model,
Prompt: "Write a long story about space exploration",
})
if err != nil {
log.Fatal(err)
}
for chunk := range stream.Chunks() {
fmt.Print(chunk.Text)
}
// Check if canceled or completed normally
if err := stream.Err(); err != nil {
if ctx.Err() == context.Canceled {
log.Println("\nOperation was canceled by user")
// Perform cleanup
savePartialResults()
} else {
log.Printf("\nStream error: %v", err)
}
} else {
log.Println("\nOperation completed successfully")
}
}
func savePartialResults() {
// Save any partial results before exiting
log.Println("Saving partial results...")
}
Context Propagation
Error: Missing Context Propagation
Symptoms:
- Child operations don't cancel when parent is canceled
- Timeouts not enforced in nested calls
Cause: Not passing context through the call chain.
Solution:
package main
import (
"context"
"fmt"
"log"
"os"
"time"
"github.com/digitallysavvy/go-ai/pkg/ai"
"github.com/digitallysavvy/go-ai/pkg/providers/openai"
)
// BAD: Creating new context instead of propagating
func processPromptBad(prompt string) (string, error) {
// This ignores any parent cancellation or timeout!
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: prompt,
})
if err != nil {
return "", err
}
return result.Text, nil
}
// GOOD: Propagating context through call chain
func processPromptGood(ctx context.Context, prompt string) (string, error) {
// Use the provided context so parent cancellation/timeout applies
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: prompt,
})
if err != nil {
return "", err
}
return result.Text, nil
}
func main() {
// Parent context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// CORRECT: Pass context to children
text, err := processPromptGood(ctx, "Explain quantum computing")
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
log.Println("Entire operation timed out")
} else {
log.Printf("Error: %v", err)
}
return
}
fmt.Println(text)
}
Streaming Context Issues
Error: Stream Doesn't Respect Context
Symptoms:
- Stream continues after context canceled
- Goroutine leak when stream not fully consumed
Cause: Not checking context in stream processing loop.
Solution:
package main
import (
"context"
"fmt"
"log"
"os"
"time"
"github.com/digitallysavvy/go-ai/pkg/ai"
"github.com/digitallysavvy/go-ai/pkg/provider"
"github.com/digitallysavvy/go-ai/pkg/providers/openai"
)
func streamWithTimeout(ctx context.Context, model provider.LanguageModel, prompt string) error {
stream, err := ai.StreamText(ctx, ai.StreamTextOptions{
Model: model,
Prompt: prompt,
})
if err != nil {
return err
}
// Process stream with context checking
for {
select {
case <-ctx.Done():
// Context canceled or timed out
log.Println("Context done, stopping stream processing")
return ctx.Err()
case chunk, ok := <-stream.Chunks():
if !ok {
// Channel closed, stream finished
return stream.Err()
}
fmt.Print(chunk.Text)
}
}
}
func main() {
provider := openai.New(openai.Config{
APIKey: os.Getenv("OPENAI_API_KEY"),
})
model, _ := provider.LanguageModel("gpt-4")
// Set timeout for entire stream
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
if err := streamWithTimeout(ctx, model, "Write a very long story"); err != nil {
if ctx.Err() == context.DeadlineExceeded {
log.Println("\nStream timed out after 15 seconds")
} else if ctx.Err() == context.Canceled {
log.Println("\nStream was canceled")
} else {
log.Printf("\nStream error: %v", err)
}
}
}
Context in Goroutines
Error: Context Not Passed to Goroutines
Symptoms:
- Goroutines don't stop when parent context canceled
- Memory leaks from orphaned goroutines
Cause: Not passing context to goroutines or creating new contexts.
Solution:
package main
import (
"context"
"fmt"
"log"
"os"
"sync"
"time"
"github.com/digitallysavvy/go-ai/pkg/ai"
"github.com/digitallysavvy/go-ai/pkg/provider"
"github.com/digitallysavvy/go-ai/pkg/providers/openai"
)
func processPrompts(ctx context.Context, model provider.LanguageModel, prompts []string) []string {
results := make([]string, len(prompts))
var wg sync.WaitGroup
for i, prompt := range prompts {
wg.Add(1)
// CORRECT: Pass context to goroutine
go func(index int, p string) {
defer wg.Done()
// Check if context already canceled before starting work
select {
case <-ctx.Done():
log.Printf("Goroutine %d: context canceled before starting", index)
return
default:
}
result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: p,
})
if err != nil {
if ctx.Err() != nil {
log.Printf("Goroutine %d: canceled during execution", index)
} else {
log.Printf("Goroutine %d: error: %v", index, err)
}
return
}
results[index] = result.Text
log.Printf("Goroutine %d: completed", index)
}(i, prompt)
}
// Wait for all goroutines with timeout awareness
done := make(chan struct{})
go func() {
wg.Wait()
close(done)
}()
select {
case <-done:
log.Println("All goroutines completed")
case <-ctx.Done():
log.Println("Context canceled, waiting for goroutines to cleanup...")
<-done // Still wait for goroutines to finish cleanup
}
return results
}
func main() {
provider := openai.New(openai.Config{
APIKey: os.Getenv("OPENAI_API_KEY"),
})
model, _ := provider.LanguageModel("gpt-4")
// Context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
prompts := []string{
"Explain AI",
"Explain ML",
"Explain DL",
"Explain NLP",
"Explain CV",
}
results := processPrompts(ctx, model, prompts)
for i, result := range results {
if result != "" {
fmt.Printf("\n=== Result %d ===\n%s\n", i, result)
}
}
}
Context Value Issues
Error: Misusing Context Values
Symptoms:
- Type assertions panic
- Values not found in context
Cause: Incorrect use of context.WithValue.
Solution:
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/digitallysavvy/go-ai/pkg/ai"
"github.com/digitallysavvy/go-ai/pkg/providers/openai"
)
// Define custom type for context keys (prevents collisions)
type contextKey string
const (
userIDKey contextKey = "userID"
requestIDKey contextKey = "requestID"
)
// Helper functions to set/get values safely
func WithUserID(ctx context.Context, userID string) context.Context {
return context.WithValue(ctx, userIDKey, userID)
}
func GetUserID(ctx context.Context) (string, bool) {
userID, ok := ctx.Value(userIDKey).(string)
return userID, ok
}
func WithRequestID(ctx context.Context, requestID string) context.Context {
return context.WithValue(ctx, requestIDKey, requestID)
}
func GetRequestID(ctx context.Context) (string, bool) {
requestID, ok := ctx.Value(requestIDKey).(string)
return requestID, ok
}
func processWithContext(ctx context.Context, prompt string) (string, error) {
// Safely extract context values
userID, hasUser := GetUserID(ctx)
requestID, hasRequest := GetRequestID(ctx)
if hasUser {
log.Printf("Processing request for user: %s", userID)
}
if hasRequest {
log.Printf("Request ID: %s", requestID)
}
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: prompt,
})
if err != nil {
return "", err
}
return result.Text, nil
}
func main() {
ctx := context.Background()
// Add request metadata to context
ctx = WithUserID(ctx, "user-123")
ctx = WithRequestID(ctx, "req-456")
text, err := processWithContext(ctx, "Explain quantum computing")
if err != nil {
log.Fatal(err)
}
fmt.Println(text)
}
Best Practices
1. Always Pass Context as First Parameter
// Good
func processPrompt(ctx context.Context, prompt string) (string, error)
// Bad
func processPrompt(prompt string) (string, error)
2. Never Store Context in Struct
// Bad - don't store context
type Processor struct {
ctx context.Context // NEVER do this
}
// Good - pass context to methods
type Processor struct {
// other fields
}
func (p *Processor) Process(ctx context.Context) error {
// use ctx parameter
}
3. Use Appropriate Timeouts
// Short operations
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
// Long operations
ctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
defer cancel()
// Streaming - no timeout or very long timeout
ctx := context.Background()
// or
ctx, cancel := context.WithTimeout(ctx, 10*time.Minute)
defer cancel()
4. Always Defer cancel()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel() // Always defer, even if context times out naturally
5. Check Context Error After Operations
result, err := ai.GenerateText(ctx, options)
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
// Handle timeout
} else if ctx.Err() == context.Canceled {
// Handle cancellation
} else {
// Handle other errors
}
}
6. Don't Ignore Context in OnFinish
stream, _ := ai.StreamText(ctx, ai.StreamTextOptions{
Model: model,
Prompt: "...",
OnFinish: func(ctx context.Context, result *ai.StreamTextResult, userContext interface{}) {
// OnFinish receives the same context - check it!
if ctx.Err() != nil {
log.Println("Context was canceled, skipping post-processing")
return
}
// Process result...
},
})
7. Use Context for Graceful Shutdown
ctx, cancel := context.WithCancel(context.Background())
// Handle shutdown signals
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
go func() {
<-sigCh
log.Println("Shutting down...")
cancel()
}()
// All operations use this context
processRequests(ctx)