Frontend Labs
⋯

Day 15 — context Package

Learning Objectives

  • Propagate cancellation and deadlines through a call stack
  • Understand why context is Go's answer to request-scoped data
  • Build timeout chains: parent cancels children automatically

Key Concepts

Request arrives
    └── ctx, cancel = context.WithTimeout(bg, 30s)
        ├── auth check ctx, _ = context.WithTimeout(ctx, 2s)
        ├── DB query    ctx, _ = context.WithTimeout(ctx, 5s)
        └── response    context.WithValue(ctx, "request_id", "abc123")

If auth takes >2s: auth context times out → only auth cancels
If parent >30s:    ALL children cancel simultaneously
// Creating contexts
ctx := context.Background()           // root, never cancels
ctx := context.TODO()                 // placeholder during refactoring

// Cancellation
ctx, cancel := context.WithCancel(parent)
defer cancel()  // ALWAYS defer cancel to avoid context leak

// Timeout
ctx, cancel := context.WithTimeout(parent, 5*time.Second)
defer cancel()

// Deadline
ctx, cancel := context.WithDeadline(parent, time.Now().Add(5*time.Second))
defer cancel()

// Request-scoped values (use typed keys to avoid collisions)
type ctxKey string
const requestIDKey ctxKey = "request_id"

ctx = context.WithValue(ctx, requestIDKey, "abc-123")
id := ctx.Value(requestIDKey).(string)

// Check cancellation
select {
case <-ctx.Done():
    return ctx.Err() // context.DeadlineExceeded or context.Canceled
default:
    // not cancelled, proceed
}

Exercise: Multi-Stage Request Pipeline

package main

import (
    "context"
    "fmt"
    "time"
)

type ctxKey string

const (
    requestIDKey ctxKey = "request_id"
    userIDKey    ctxKey = "user_id"
)

// authenticate validates credentials — timeout: 2s
func authenticate(ctx context.Context, token string) (string, error) {
    authCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
    defer cancel()

    // Simulate DB lookup
    done := make(chan string, 1)
    go func() {
        time.Sleep(50 * time.Millisecond) // fast auth
        done <- "user:42"
    }()

    select {
    case userID := <-done:
        return userID, nil
    case <-authCtx.Done():
        return "", fmt.Errorf("authenticate: %w", authCtx.Err())
    }
}

// fetchData retrieves data — timeout: 5s
func fetchData(ctx context.Context, userID string) (map[string]string, error) {
    dataCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()

    done := make(chan map[string]string, 1)
    go func() {
        time.Sleep(100 * time.Millisecond)
        done <- map[string]string{"name": "Alice", "role": "admin"}
    }()

    select {
    case data := <-done:
        return data, nil
    case <-dataCtx.Done():
        return nil, fmt.Errorf("fetchData: %w", dataCtx.Err())
    }
}

func handleRequest(ctx context.Context, requestID, token string) error {
    // Inject request ID into context — available to all downstream functions
    ctx = context.WithValue(ctx, requestIDKey, requestID)

    // Stage 1: auth
    userID, err := authenticate(ctx, token)
    if err != nil {
        return fmt.Errorf("[%s] auth failed: %w", requestID, err)
    }
    ctx = context.WithValue(ctx, userIDKey, userID)
    fmt.Printf("[%s] authenticated as %s\n", requestID, userID)

    // Stage 2: fetch data
    data, err := fetchData(ctx, userID)
    if err != nil {
        return fmt.Errorf("[%s] data fetch failed: %w", requestID, err)
    }
    fmt.Printf("[%s] data: %v\n", requestID, data)
    return nil
}

func main() {
    // Normal request — 30s budget, well within timeout
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    if err := handleRequest(ctx, "req-001", "valid-token"); err != nil {
        fmt.Println("ERROR:", err)
    }

    // Tight deadline — will fail
    tightCtx, tightCancel := context.WithTimeout(context.Background(), 1*time.Millisecond)
    defer tightCancel()
    if err := handleRequest(tightCtx, "req-002", "valid-token"); err != nil {
        fmt.Println("TIMEOUT:", err)
    }
}

Real-World Context

Every HTTP handler in your API gateway receives a context from net/http. The context is cancelled when the client disconnects. Without checking ctx.Done(), your handler keeps running, consuming DB connections and CPU, even though no one will receive the response. Always propagate context.


Generated from week3.md · 8 Oct 2026 16:36