Skip to content

Context Integration

Understanding how to effectively integrate error handling with Go's context package is crucial for building robust, context-aware applications. Context integration allows us to carry request-scoped data, handle timeouts gracefully, and maintain traceability throughout our application's error handling flow.

Understanding Context in Error Handling

Go's context package serves multiple purposes in error handling:

  • Carrying request-scoped values (like request IDs or user information)
  • Managing timeouts and cancellation
  • Ensuring proper resource cleanup
  • Maintaining traceability across service boundaries

Let's explore how ewrap integrates with context to enhance error handling capabilities.

Basic Context Integration

The most straightforward way to integrate context with error handling is through the WithContext option:

func processUserRequest(ctx context.Context, userID string) error {
    // Create an error with context
    if err := validateUser(userID); err != nil {
        return ewrap.Wrap(err, "user validation failed",
            ewrap.WithContext(ctx, ErrorTypeValidation, SeverityError))
    }

    return nil
}

When you add context to an error, ewrap automatically extracts and preserves important context values such as:

  • Request IDs for tracing
  • User information for auditing
  • Operation metadata for monitoring
  • Timing information for performance tracking

Advanced Context Usage

Let's look at more sophisticated ways to use context in error handling:

// Define context keys for common values
type contextKey string

const (
    requestIDKey contextKey = "request_id"
    userIDKey    contextKey = "user_id"
    traceIDKey   contextKey = "trace_id"
)

// RequestContext enriches context with standard fields
func RequestContext(ctx context.Context, requestID, userID string) context.Context {
    ctx = context.WithValue(ctx, requestIDKey, requestID)
    ctx = context.WithValue(ctx, userIDKey, userID)
    ctx = context.WithValue(ctx, traceIDKey, generateTraceID())
    return ctx
}

// ContextualOperation shows how to use context throughout an operation
func ContextualOperation(ctx context.Context) error {
    // Extract context values
    requestID := ctx.Value(requestIDKey).(string)
    userID := ctx.Value(userIDKey).(string)

    // Create an operation-specific context
    opCtx := ewrap.WithOperationContext(ctx, "user_update")

    // Start a timed operation
    timer := time.Now()

    // Perform the operation with timeout
    if err := performTimedOperation(opCtx); err != nil {
        return ewrap.Wrap(err, "operation failed",
            ewrap.WithContext(ctx, ErrorTypeInternal, SeverityError)).
            WithMetadata("request_id", requestID).
            WithMetadata("user_id", userID).
            WithMetadata("duration_ms", time.Since(timer).Milliseconds())
    }

    return nil
}

Context-Aware Error Groups

Error groups can be made context-aware to handle cancellation and timeouts:

// ContextualErrorGroup manages errors with context awareness
type ContextualErrorGroup struct {
    *ewrap.ErrorGroup
    ctx context.Context
}

// NewContextualErrorGroup creates a context-aware error group
func NewContextualErrorGroup(ctx context.Context, pool *ewrap.ErrorGroupPool) *ContextualErrorGroup {
    return &ContextualErrorGroup{
        ErrorGroup: pool.Get(),
        ctx:       ctx,
    }
}

// ProcessWithContext demonstrates context-aware parallel processing
func ProcessWithContext(ctx context.Context, items []Item) error {
    pool := ewrap.NewErrorGroupPool(len(items))
    group := NewContextualErrorGroup(ctx, pool)
    defer group.Release()

    var wg sync.WaitGroup
    for _, item := range items {
        wg.Add(1)
        go func(item Item) {
            defer wg.Done()

            // Check context cancellation
            select {
            case <-ctx.Done():
                group.Add(ewrap.New("operation cancelled",
                    ewrap.WithContext(ctx, ErrorTypeInternal, SeverityWarning)))
                return
            default:
                if err := processItem(ctx, item); err != nil {
                    group.Add(err)
                }
            }
        }(item)
    }

    wg.Wait()
    return group.Error()
}

Timeout and Cancellation Handling

Proper context integration includes handling timeouts and cancellation gracefully:

// TimeoutAwareOperation shows how to handle context timeouts
func TimeoutAwareOperation(ctx context.Context) error {
    // Create a timeout context
    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()

    // Channel for operation result
    resultCh := make(chan error, 1)

    // Start the operation
    go func() {
        resultCh <- performLongOperation(ctx)
    }()

    // Wait for result or timeout
    select {
    case err := <-resultCh:
        if err != nil {
            return ewrap.Wrap(err, "operation failed",
                ewrap.WithContext(ctx, ErrorTypeInternal, SeverityError))
        }
        return nil
    case <-ctx.Done():
        return ewrap.New("operation timed out",
            ewrap.WithContext(ctx, ErrorTypeTimeout, SeverityCritical)).
            WithMetadata("timeout", 5*time.Second)
    }
}

Context Propagation in Middleware

Context integration is particularly useful in middleware chains:

// ErrorHandlingMiddleware demonstrates context propagation
func ErrorHandlingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Create request context with tracing
        ctx := r.Context()
        requestID := generateRequestID()
        ctx = context.WithValue(ctx, requestIDKey, requestID)

        // Create error group for request
        pool := ewrap.NewErrorGroupPool(4)
        eg := pool.Get()
        defer eg.Release()

        // Wrap handler execution
        err := func() error {
            // Add request timing
            timer := time.Now()
            defer func() {
                if err := recover(); err != nil {
                    eg.Add(ewrap.New("panic in handler",
                        ewrap.WithContext(ctx, ErrorTypeInternal, SeverityCritical)).
                        WithMetadata("panic_value", err).
                        WithMetadata("stack", debug.Stack()))
                }
            }()

            // Execute handler
            next.ServeHTTP(w, r.WithContext(ctx))

            // Record timing
            duration := time.Since(timer)
            if duration > time.Second {
                eg.Add(ewrap.New("slow request",
                    ewrap.WithContext(ctx, ErrorTypePerformance, SeverityWarning)).
                    WithMetadata("duration_ms", duration.Milliseconds()))
            }

            return nil
        }()

        if err != nil {
            eg.Add(err)
        }

        // Handle any collected errors
        if eg.HasErrors() {
            handleRequestErrors(w, eg.Error())
        }
    })
}

Best Practices

1. Consistent Context Propagation

Maintain consistent context handling throughout your application:

// ContextualService demonstrates consistent context handling
type ContextualService struct {
    db  *Database
    log Logger
}

func (s *ContextualService) ProcessRequest(ctx context.Context, req Request) error {
    // Enrich context with request information
    ctx = enrichContext(ctx, req)

    // Use context in all operations
    if err := s.validateRequest(ctx, req); err != nil {
        return ewrap.Wrap(err, "validation failed",
            ewrap.WithContext(ctx, ErrorTypeValidation, SeverityError))
    }

    if err := s.processData(ctx, req.Data); err != nil {
        return ewrap.Wrap(err, "processing failed",
            ewrap.WithContext(ctx, ErrorTypeInternal, SeverityError))
    }

    return nil
}

2. Context Value Management

Be careful with context values and provide type-safe accessors:

// RequestInfo holds request-specific context values
type RequestInfo struct {
    RequestID string
    UserID    string
    TraceID   string
    StartTime time.Time
}

// GetRequestInfo safely extracts request information from context
func GetRequestInfo(ctx context.Context) (RequestInfo, bool) {
    info, ok := ctx.Value(requestInfoKey).(RequestInfo)
    return info, ok
}

// WithRequestInfo adds request information to context
func WithRequestInfo(ctx context.Context, info RequestInfo) context.Context {
    return context.WithValue(ctx, requestInfoKey, info)
}

3. Error Context Enrichment

Systematically enrich errors with context information:

// EnrichError adds standard context information to errors
func EnrichError(ctx context.Context, err error) error {
    if err == nil {
        return nil
    }

    info, ok := GetRequestInfo(ctx)
    if !ok {
        return err
    }

    return ewrap.Wrap(err, "operation failed",
        ewrap.WithContext(ctx, getErrorType(err), getSeverity(err))).
        WithMetadata("request_id", info.RequestID).
        WithMetadata("user_id", info.UserID).
        WithMetadata("trace_id", info.TraceID).
        WithMetadata("duration_ms", time.Since(info.StartTime).Milliseconds())
}

The context integration in ewrap provides a robust foundation for error tracking and debugging. By consistently using these features, you can build applications that are easier to monitor, debug, and maintain.