Last active
February 25, 2026 16:36
-
-
Save rednafi/3d829a0e1aaa6ae6219c02d70168e165 to your computer and use it in GitHub Desktop.
Code examples for 'Debugging context cancellation in Go' — https://rednafi.com/go/context-cancellation-cause/
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Manual timer pattern: WithCancelCause + time.AfterFunc covers every path. | |
| package main | |
| import ( | |
| "context" | |
| "errors" | |
| "fmt" | |
| "time" | |
| ) | |
| func checkInventory(ctx context.Context, orderID string) error { | |
| select { | |
| case <-ctx.Done(): | |
| return ctx.Err() | |
| case <-time.After(50 * time.Millisecond): | |
| return nil | |
| } | |
| } | |
| func checkInventoryFail(ctx context.Context, orderID string) error { | |
| select { | |
| case <-ctx.Done(): | |
| return ctx.Err() | |
| case <-time.After(50 * time.Millisecond): | |
| return fmt.Errorf("connection refused") | |
| } | |
| } | |
| func chargePayment(ctx context.Context, orderID string) error { | |
| select { | |
| case <-ctx.Done(): | |
| return ctx.Err() | |
| case <-time.After(50 * time.Millisecond): | |
| return nil | |
| } | |
| } | |
| func shipOrder(ctx context.Context, orderID string) error { | |
| return nil | |
| } | |
| func processOrder( | |
| ctx context.Context, | |
| orderID string, | |
| timeout time.Duration, | |
| inventoryFn func(context.Context, string) error, | |
| ) error { | |
| ctx, cancel := context.WithCancelCause(ctx) // (1) | |
| defer cancel(errors.New("processOrder completed")) // (2) | |
| timer := time.AfterFunc(timeout, func() { | |
| cancel(fmt.Errorf("order %s: %s timeout exceeded", orderID, timeout)) // (3) | |
| }) | |
| defer timer.Stop() // (4) | |
| if err := inventoryFn(ctx, orderID); err != nil { | |
| cancel(fmt.Errorf("order %s: inventory check failed: %w", orderID, err)) | |
| return err | |
| } | |
| if err := chargePayment(ctx, orderID); err != nil { | |
| cancel(fmt.Errorf("order %s: payment failed: %w", orderID, err)) | |
| return err | |
| } | |
| return shipOrder(ctx, orderID) | |
| } | |
| func main() { | |
| fmt.Println("=== Path 1: timeout fires ===") | |
| { | |
| ctx, cancel := context.WithCancelCause(context.Background()) | |
| defer cancel(nil) | |
| // Use a very short timeout so it fires before work completes. | |
| ctx2, cancel2 := context.WithCancelCause(ctx) | |
| defer cancel2(errors.New("processOrder completed")) | |
| timer := time.AfterFunc(30*time.Millisecond, func() { | |
| cancel2(fmt.Errorf("order ord-123: 30ms timeout exceeded")) | |
| }) | |
| defer timer.Stop() | |
| // Simulate slow work. | |
| time.Sleep(60 * time.Millisecond) | |
| _ = timer | |
| fmt.Println("ctx.Err():", ctx2.Err()) | |
| fmt.Println("cause: ", context.Cause(ctx2)) | |
| // cause = order ord-123: 30ms timeout exceeded | |
| } | |
| fmt.Println() | |
| fmt.Println("=== Path 2: inventory check fails ===") | |
| { | |
| ctx := context.Background() | |
| err := processOrder(ctx, "ord-456", 5*time.Second, checkInventoryFail) | |
| fmt.Println("returned error:", err) | |
| // error: connection refused | |
| // (the cause inside the context is "order ord-456: inventory check failed: connection refused") | |
| } | |
| fmt.Println() | |
| fmt.Println("=== Path 3: normal completion ===") | |
| { | |
| ctx, cancel := context.WithCancelCause(context.Background()) | |
| defer cancel(nil) | |
| // Run processOrder with a context we control so we can read the cause. | |
| err := processOrder(ctx, "ord-789", 5*time.Second, checkInventory) | |
| fmt.Println("returned error:", err) | |
| fmt.Println("cause: ", context.Cause(ctx)) | |
| // error: <nil> | |
| // The inner context's cause is "processOrder completed" but | |
| // the outer context is uncanceled so cause is nil. | |
| } | |
| fmt.Println() | |
| fmt.Println("=== Full demo with readable cause from middleware perspective ===") | |
| { | |
| // Simulate a middleware that wraps the context. | |
| outerCtx, outerCancel := context.WithCancelCause(context.Background()) | |
| defer outerCancel(errors.New("request completed")) | |
| // processOrder creates its own inner context, but if we pass | |
| // the outer context, the inner cancel propagates to it. | |
| // In practice, the handler would call outerCancel with a specific cause. | |
| _ = outerCtx | |
| // Demonstrate: the manual timer pattern ensures ctx.Err() is always Canceled. | |
| ctx, cancel := context.WithCancelCause(context.Background()) | |
| timer := time.AfterFunc(5*time.Second, func() { | |
| cancel(fmt.Errorf("5s timeout exceeded")) | |
| }) | |
| cancel(errors.New("completed normally")) | |
| timer.Stop() | |
| fmt.Println("ctx.Err():", ctx.Err()) | |
| fmt.Println("cause: ", context.Cause(ctx)) | |
| // ctx.Err() = context canceled (always Canceled, never DeadlineExceeded) | |
| // cause = completed normally | |
| } | |
| } | |
| // Output: | |
| // | |
| // === Path 1: timeout fires === | |
| // ctx.Err(): context canceled | |
| // cause: order ord-123: 30ms timeout exceeded | |
| // | |
| // === Path 2: inventory check fails === | |
| // returned error: connection refused | |
| // | |
| // === Path 3: normal completion === | |
| // returned error: <nil> | |
| // cause: <nil> | |
| // | |
| // === Full demo with readable cause from middleware perspective === | |
| // ctx.Err(): context canceled | |
| // cause: completed normally |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // The debugging problem: context canceled tells you nothing about why. | |
| package main | |
| import ( | |
| "context" | |
| "fmt" | |
| "net" | |
| "time" | |
| ) | |
| func checkInventory(ctx context.Context, orderID string) error { | |
| // Simulate an HTTP call that fails because the context was canceled. | |
| select { | |
| case <-ctx.Done(): | |
| return ctx.Err() | |
| case <-time.After(50 * time.Millisecond): | |
| return nil | |
| } | |
| } | |
| func chargePayment(ctx context.Context, orderID string) error { | |
| // Simulate a slow payment gateway that exceeds the timeout. | |
| select { | |
| case <-ctx.Done(): | |
| return ctx.Err() | |
| case <-time.After(200 * time.Millisecond): | |
| return nil | |
| } | |
| } | |
| func shipOrder(ctx context.Context, orderID string) error { | |
| return nil | |
| } | |
| func processOrder(ctx context.Context, orderID string) error { | |
| ctx, cancel := context.WithTimeout(ctx, 5*time.Second) // (1) | |
| defer cancel() // (2) | |
| if err := checkInventory(ctx, orderID); err != nil { | |
| return err // (3) | |
| } | |
| if err := chargePayment(ctx, orderID); err != nil { | |
| return err | |
| } | |
| return shipOrder(ctx, orderID) | |
| } | |
| func processOrderWrapped(ctx context.Context, orderID string) error { | |
| ctx, cancel := context.WithTimeout(ctx, 5*time.Second) | |
| defer cancel() | |
| if err := checkInventory(ctx, orderID); err != nil { | |
| return fmt.Errorf("checking inventory for %s: %w", orderID, err) | |
| } | |
| if err := chargePayment(ctx, orderID); err != nil { | |
| return fmt.Errorf("charging payment for %s: %w", orderID, err) | |
| } | |
| return shipOrder(ctx, orderID) | |
| } | |
| func main() { | |
| fmt.Println("=== Scenario 1: client disconnects during inventory check ===") | |
| { | |
| ctx, cancel := context.WithCancel(context.Background()) | |
| // Cancel immediately to simulate client disconnect. | |
| cancel() | |
| err := processOrder(ctx, "ord-123") | |
| fmt.Println("error:", err) | |
| // Output: context canceled | |
| } | |
| fmt.Println() | |
| fmt.Println("=== Scenario 2: timeout fires during slow payment ===") | |
| { | |
| // Use a very short timeout so chargePayment exceeds it. | |
| ctx, cancel := context.WithTimeout(context.Background(), 80*time.Millisecond) | |
| defer cancel() | |
| err := processOrder(ctx, "ord-456") | |
| fmt.Println("error:", err) | |
| // Output: context deadline exceeded | |
| } | |
| fmt.Println() | |
| fmt.Println("=== Scenario 3: wrapping doesn't explain WHY ===") | |
| { | |
| ctx, cancel := context.WithCancel(context.Background()) | |
| cancel() | |
| err := processOrderWrapped(ctx, "ord-789") | |
| fmt.Println("error:", err) | |
| // Output: checking inventory for ord-789: context canceled | |
| // You know WHERE but not WHY. | |
| } | |
| fmt.Println() | |
| fmt.Println("=== Scenario 4: network error also becomes opaque ===") | |
| { | |
| // Simulate a network error wrapped in context cancellation. | |
| ctx, cancel := context.WithCancel(context.Background()) | |
| cancel() | |
| err := fmt.Errorf("checking inventory: %w", | |
| fmt.Errorf("dial tcp: %w", | |
| &net.OpError{Op: "dial", Net: "tcp", Err: fmt.Errorf("connection refused")})) | |
| _ = ctx | |
| fmt.Println("error:", err) | |
| } | |
| } | |
| // Output: | |
| // | |
| // === Scenario 1: client disconnects during inventory check === | |
| // error: context canceled | |
| // | |
| // === Scenario 2: timeout fires during slow payment === | |
| // error: context deadline exceeded | |
| // | |
| // === Scenario 3: wrapping doesn't explain WHY === | |
| // error: checking inventory for ord-789: context canceled | |
| // | |
| // === Scenario 4: network error also becomes opaque === | |
| // error: checking inventory: dial tcp: dial tcp: connection refused |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Reading the cause: errors.Is, errors.As, and structured logging. | |
| package main | |
| import ( | |
| "context" | |
| "errors" | |
| "fmt" | |
| "log/slog" | |
| "net" | |
| "os" | |
| ) | |
| func main() { | |
| logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) | |
| fmt.Println("=== errors.As: unwrapping a *net.OpError from the cause ===") | |
| { | |
| ctx, cancel := context.WithCancelCause(context.Background()) | |
| // Simulate: cancel with a cause that wraps a *net.OpError. | |
| opErr := &net.OpError{ | |
| Op: "dial", | |
| Net: "tcp", | |
| Addr: &net.TCPAddr{ | |
| IP: net.ParseIP("10.0.0.1"), | |
| Port: 8080, | |
| }, | |
| Err: fmt.Errorf("connection refused"), | |
| } | |
| cancel(fmt.Errorf("order ord-123: inventory check failed: %w", opErr)) | |
| cause := context.Cause(ctx) | |
| fmt.Println("cause:", cause) | |
| var netErr *net.OpError | |
| if errors.As(cause, &netErr) { | |
| fmt.Println("errors.As matched *net.OpError:") | |
| fmt.Println(" op: ", netErr.Op) | |
| fmt.Println(" addr:", netErr.Addr) | |
| fmt.Println(" err: ", netErr.Err) | |
| } | |
| } | |
| fmt.Println() | |
| fmt.Println("=== errors.Is: checking for DeadlineExceeded in the cause ===") | |
| { | |
| ctx, cancel := context.WithCancelCause(context.Background()) | |
| // Wrap DeadlineExceeded in the cause so errors.Is can find it. | |
| cancel(fmt.Errorf("order timeout: %w", context.DeadlineExceeded)) | |
| cause := context.Cause(ctx) | |
| fmt.Println("cause:", cause) | |
| fmt.Println("errors.Is(cause, DeadlineExceeded):", errors.Is(cause, context.DeadlineExceeded)) | |
| } | |
| fmt.Println() | |
| fmt.Println("=== errors.Is: cause does NOT match DeadlineExceeded without wrapping ===") | |
| { | |
| ctx, cancel := context.WithCancelCause(context.Background()) | |
| // Cancel with a plain error (no %w of DeadlineExceeded). | |
| cancel(fmt.Errorf("order ord-456: 5s timeout exceeded")) | |
| cause := context.Cause(ctx) | |
| fmt.Println("cause:", cause) | |
| fmt.Println("errors.Is(cause, DeadlineExceeded):", errors.Is(cause, context.DeadlineExceeded)) | |
| // false — the cause doesn't wrap DeadlineExceeded. | |
| } | |
| fmt.Println() | |
| fmt.Println("=== Structured logging: ctx.Err() + context.Cause() as separate fields ===") | |
| { | |
| ctx, cancel := context.WithCancelCause(context.Background()) | |
| cancel(fmt.Errorf("order ord-789: inventory check failed: connection refused")) | |
| if ctx.Err() != nil { | |
| logger.Error("request failed", | |
| "err", ctx.Err(), | |
| "cause", context.Cause(ctx), | |
| ) | |
| } | |
| // Output: level=ERROR msg="request failed" err="context canceled" | |
| // cause="order ord-789: inventory check failed: connection refused" | |
| } | |
| fmt.Println() | |
| fmt.Println("=== Cause on non-cause context returns ctx.Err() ===") | |
| { | |
| ctx, cancel := context.WithCancel(context.Background()) | |
| cancel() | |
| fmt.Println("ctx.Err(): ", ctx.Err()) | |
| fmt.Println("context.Cause():", context.Cause(ctx)) | |
| // Both return context.Canceled. | |
| } | |
| fmt.Println() | |
| fmt.Println("=== Cause on uncanceled context returns nil ===") | |
| { | |
| ctx := context.Background() | |
| fmt.Println("ctx.Err(): ", ctx.Err()) | |
| fmt.Println("context.Cause():", context.Cause(ctx)) | |
| // Both nil. | |
| } | |
| } | |
| // Output: | |
| // | |
| // === errors.As: unwrapping a *net.OpError from the cause === | |
| // cause: order ord-123: inventory check failed: dial tcp 10.0.0.1:8080: connection refused | |
| // errors.As matched *net.OpError: | |
| // op: dial | |
| // addr: 10.0.0.1:8080 | |
| // err: connection refused | |
| // | |
| // === errors.Is: checking for DeadlineExceeded in the cause === | |
| // cause: order timeout: context deadline exceeded | |
| // errors.Is(cause, DeadlineExceeded): true | |
| // | |
| // === errors.Is: cause does NOT match DeadlineExceeded without wrapping === | |
| // cause: order ord-456: 5s timeout exceeded | |
| // errors.Is(cause, DeadlineExceeded): false | |
| // | |
| // === Structured logging: ctx.Err() + context.Cause() as separate fields === | |
| // time=... level=ERROR msg="request failed" err="context canceled" cause="order ord-789: inventory check failed: connection refused" | |
| // | |
| // === Cause on non-cause context returns ctx.Err() === | |
| // ctx.Err(): context canceled | |
| // context.Cause(): context canceled | |
| // | |
| // === Cause on uncanceled context returns nil === | |
| // ctx.Err(): <nil> | |
| // context.Cause(): <nil> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // HTTP middleware pattern: automatic cause tracking for every request. | |
| package main | |
| import ( | |
| "context" | |
| "errors" | |
| "fmt" | |
| "log/slog" | |
| "net" | |
| "net/http" | |
| "os" | |
| "time" | |
| ) | |
| func withCause(next http.Handler) http.Handler { | |
| return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| ctx, cancel := context.WithCancelCause(r.Context()) // (1) | |
| defer cancel(errors.New("request completed")) // (2) | |
| next.ServeHTTP(w, r.WithContext(ctx)) | |
| if ctx.Err() != nil { // (3) | |
| slog.Error("request context canceled", | |
| "method", r.Method, | |
| "path", r.URL.Path, | |
| "err", ctx.Err(), | |
| "cause", context.Cause(ctx), | |
| ) | |
| } | |
| }) | |
| } | |
| func orderHandler(w http.ResponseWriter, r *http.Request) { | |
| ctx := r.Context() | |
| // Simulate processOrder with a manual timer. | |
| ctx, cancel := context.WithCancelCause(ctx) | |
| defer cancel(errors.New("processOrder completed")) | |
| timer := time.AfterFunc(5*time.Second, func() { | |
| cancel(fmt.Errorf("order processing: 5s timeout exceeded")) | |
| }) | |
| defer timer.Stop() | |
| // Simulate a slow inventory check that fails. | |
| time.Sleep(50 * time.Millisecond) | |
| inventoryErr := fmt.Errorf("connection refused") | |
| cancel(fmt.Errorf("order ord-123: inventory check failed: %w", inventoryErr)) | |
| http.Error(w, "order processing failed", http.StatusInternalServerError) | |
| } | |
| func healthHandler(w http.ResponseWriter, r *http.Request) { | |
| fmt.Fprintln(w, "ok") | |
| } | |
| func main() { | |
| logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) | |
| slog.SetDefault(logger) | |
| mux := http.NewServeMux() | |
| mux.HandleFunc("/order", orderHandler) | |
| mux.HandleFunc("/health", healthHandler) | |
| handler := withCause(mux) | |
| // Start the server in a goroutine. | |
| server := &http.Server{ | |
| Addr: "127.0.0.1:0", | |
| Handler: handler, | |
| } | |
| // Use a listener to get the actual port. | |
| ln, err := (&net.ListenConfig{}).Listen(context.Background(), "tcp", "127.0.0.1:0") | |
| if err != nil { | |
| fmt.Println("failed to listen:", err) | |
| return | |
| } | |
| addr := ln.Addr().String() | |
| fmt.Println("server listening on", addr) | |
| go func() { | |
| if err := server.Serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) { | |
| fmt.Println("server error:", err) | |
| } | |
| }() | |
| // Give the server a moment to start. | |
| time.Sleep(50 * time.Millisecond) | |
| // Test 1: hit the order endpoint (triggers cause logging). | |
| fmt.Println() | |
| fmt.Println("=== GET /order (handler sets a specific cause) ===") | |
| { | |
| resp, err := http.Get("http://" + addr + "/order") | |
| if err != nil { | |
| fmt.Println("request error:", err) | |
| } else { | |
| fmt.Println("response status:", resp.Status) | |
| resp.Body.Close() | |
| } | |
| } | |
| // Test 2: hit the health endpoint (no cancellation). | |
| fmt.Println() | |
| fmt.Println("=== GET /health (no cancellation, cause is nil) ===") | |
| { | |
| resp, err := http.Get("http://" + addr + "/health") | |
| if err != nil { | |
| fmt.Println("request error:", err) | |
| } else { | |
| fmt.Println("response status:", resp.Status) | |
| resp.Body.Close() | |
| } | |
| } | |
| // Shut down. | |
| ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) | |
| defer cancel() | |
| server.Shutdown(ctx) | |
| fmt.Println() | |
| fmt.Println("server shut down") | |
| } | |
| // Output: | |
| // | |
| // server listening on 127.0.0.1:xxxxx | |
| // | |
| // === GET /order (handler sets a specific cause) === | |
| // response status: 500 Internal Server Error | |
| // | |
| // === GET /health (no cancellation, cause is nil) === | |
| // response status: 200 OK | |
| // | |
| // server shut down |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Stacked contexts: WithCancelCause + WithTimeoutCause preserves DeadlineExceeded. | |
| package main | |
| import ( | |
| "context" | |
| "errors" | |
| "fmt" | |
| "time" | |
| ) | |
| func checkInventory(ctx context.Context, orderID string) error { | |
| select { | |
| case <-ctx.Done(): | |
| return ctx.Err() | |
| case <-time.After(50 * time.Millisecond): | |
| return nil | |
| } | |
| } | |
| func chargePayment(ctx context.Context, orderID string) error { | |
| select { | |
| case <-ctx.Done(): | |
| return ctx.Err() | |
| case <-time.After(50 * time.Millisecond): | |
| return nil | |
| } | |
| } | |
| func shipOrder(ctx context.Context, orderID string) error { | |
| return nil | |
| } | |
| func main() { | |
| fmt.Println("=== Path 1: timeout fires (DeadlineExceeded preserved) ===") | |
| { | |
| ctx, cancelCause := context.WithCancelCause(context.Background()) // (1) | |
| ctx, cancelTimeout := context.WithTimeoutCause( // (2) | |
| ctx, | |
| 50*time.Millisecond, | |
| fmt.Errorf("order ord-123: 50ms timeout exceeded"), | |
| ) | |
| defer cancelTimeout() // (3) | |
| defer cancelCause(errors.New("processOrder completed")) // (4) | |
| // Simulate slow work that exceeds the timeout. | |
| time.Sleep(80 * time.Millisecond) | |
| fmt.Println("ctx.Err(): ", ctx.Err()) | |
| fmt.Println("cause: ", context.Cause(ctx)) | |
| fmt.Println("is DeadlineExceeded:", errors.Is(ctx.Err(), context.DeadlineExceeded)) | |
| // ctx.Err() = context deadline exceeded ✓ | |
| // cause = order ord-123: 50ms timeout exceeded ✓ | |
| // errors.Is = true ✓ | |
| } | |
| fmt.Println() | |
| fmt.Println("=== Path 2: error path (cancelCause with specific error) ===") | |
| { | |
| ctx, cancelCause := context.WithCancelCause(context.Background()) | |
| ctx, cancelTimeout := context.WithTimeoutCause( | |
| ctx, | |
| 5*time.Second, | |
| fmt.Errorf("order ord-456: 5s timeout exceeded"), | |
| ) | |
| defer cancelTimeout() | |
| defer cancelCause(errors.New("processOrder completed")) | |
| // Simulate an inventory check failure. | |
| inventoryErr := fmt.Errorf("connection refused") | |
| cancelCause(fmt.Errorf("order ord-456: inventory check failed: %w", inventoryErr)) | |
| fmt.Println("ctx.Err(): ", ctx.Err()) | |
| fmt.Println("cause: ", context.Cause(ctx)) | |
| fmt.Println("is DeadlineExceeded:", errors.Is(ctx.Err(), context.DeadlineExceeded)) | |
| // ctx.Err() = context canceled (parent canceled, propagated to inner) | |
| // cause = order ord-456: inventory check failed: connection refused ✓ | |
| // errors.Is = false (not a timeout) | |
| } | |
| fmt.Println() | |
| fmt.Println("=== Path 3: normal completion (LIFO defer ordering) ===") | |
| { | |
| ctx, cancelCause := context.WithCancelCause(context.Background()) | |
| ctx, cancelTimeout := context.WithTimeoutCause( | |
| ctx, | |
| 5*time.Second, | |
| fmt.Errorf("order ord-789: 5s timeout exceeded"), | |
| ) | |
| // LIFO: cancelCause runs first, cancelTimeout runs second. | |
| defer cancelTimeout() | |
| defer cancelCause(errors.New("processOrder completed")) | |
| // Simulate successful completion. | |
| _ = checkInventory(ctx, "ord-789") | |
| _ = chargePayment(ctx, "ord-789") | |
| _ = shipOrder(ctx, "ord-789") | |
| // Defers haven't run yet, context should still be active. | |
| fmt.Println("before defers - ctx.Err():", ctx.Err()) | |
| fmt.Println("before defers - cause: ", context.Cause(ctx)) | |
| } | |
| // After the block exits, defers run: | |
| // 1. cancelCause("processOrder completed") cancels outer, propagates to inner | |
| // 2. cancelTimeout() finds inner already canceled, no-op | |
| fmt.Println("(defers ran: cancelCause first, then cancelTimeout)") | |
| fmt.Println() | |
| fmt.Println("=== Demonstrating wrong defer order ===") | |
| { | |
| ctx, cancelCause := context.WithCancelCause(context.Background()) | |
| ctx, cancelTimeout := context.WithTimeoutCause( | |
| ctx, | |
| 5*time.Second, | |
| fmt.Errorf("order ord-wrong: 5s timeout exceeded"), | |
| ) | |
| // WRONG ORDER: cancelTimeout deferred second, so it runs first. | |
| defer cancelCause(errors.New("processOrder completed")) | |
| defer cancelTimeout() // This runs first! | |
| // After this block, cancelTimeout() runs first and cancels the inner | |
| // context with context.Canceled (no cause). Then cancelCause runs | |
| // but the inner context is already canceled. | |
| _ = ctx | |
| } | |
| fmt.Println("(wrong defer order: cancelTimeout ran first, discarding the cause)") | |
| } | |
| // Output: | |
| // | |
| // === Path 1: timeout fires (DeadlineExceeded preserved) === | |
| // ctx.Err(): context deadline exceeded | |
| // cause: order ord-123: 50ms timeout exceeded | |
| // is DeadlineExceeded: true | |
| // | |
| // === Path 2: error path (cancelCause with specific error) === | |
| // ctx.Err(): context canceled | |
| // cause: order ord-456: inventory check failed: connection refused | |
| // is DeadlineExceeded: false | |
| // | |
| // === Path 3: normal completion (LIFO defer ordering) === | |
| // before defers - ctx.Err(): <nil> | |
| // before defers - cause: <nil> | |
| // (defers ran: cancelCause first, then cancelTimeout) | |
| // | |
| // === Demonstrating wrong defer order === | |
| // (wrong defer order: cancelTimeout ran first, discarding the cause) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // WithCancelCause: attaching a reason to context cancellation. | |
| package main | |
| import ( | |
| "context" | |
| "fmt" | |
| "time" | |
| ) | |
| func checkInventory(ctx context.Context, orderID string) error { | |
| select { | |
| case <-ctx.Done(): | |
| return ctx.Err() | |
| case <-time.After(50 * time.Millisecond): | |
| return fmt.Errorf("connection refused") | |
| } | |
| } | |
| func chargePayment(ctx context.Context, orderID string) error { | |
| select { | |
| case <-ctx.Done(): | |
| return ctx.Err() | |
| case <-time.After(50 * time.Millisecond): | |
| return nil | |
| } | |
| } | |
| func shipOrder(ctx context.Context, orderID string) error { | |
| return nil | |
| } | |
| func processOrder(ctx context.Context, orderID string) error { | |
| ctx, cancel := context.WithCancelCause(ctx) | |
| defer cancel(nil) // (1) | |
| if err := checkInventory(ctx, orderID); err != nil { | |
| cancel(fmt.Errorf( | |
| "order %s: inventory check failed: %w", orderID, err, | |
| )) // (2) | |
| return err | |
| } | |
| if err := chargePayment(ctx, orderID); err != nil { | |
| cancel(fmt.Errorf( | |
| "order %s: payment failed: %w", orderID, err, | |
| )) | |
| return err | |
| } | |
| return shipOrder(ctx, orderID) | |
| } | |
| func main() { | |
| fmt.Println("=== Scenario 1: inventory check fails with connection error ===") | |
| { | |
| ctx := context.Background() | |
| err := processOrder(ctx, "ord-123") | |
| fmt.Println("returned error:", err) | |
| // The cause was set before returning, but we can't read it from | |
| // outside because the context is scoped to processOrder. | |
| // In practice, you'd read it from middleware wrapping the request context. | |
| } | |
| fmt.Println() | |
| fmt.Println("=== Scenario 2: demonstrating first-cancel-wins ===") | |
| { | |
| ctx, cancel := context.WithCancelCause(context.Background()) | |
| // First cancel sets the cause. | |
| cancel(fmt.Errorf("first reason")) | |
| fmt.Println("cause after first cancel:", context.Cause(ctx)) | |
| // Second cancel is a no-op. | |
| cancel(fmt.Errorf("second reason")) | |
| fmt.Println("cause after second cancel:", context.Cause(ctx)) | |
| // Still "first reason". | |
| } | |
| fmt.Println() | |
| fmt.Println("=== Scenario 3: cancel(nil) sets cause to context.Canceled ===") | |
| { | |
| ctx, cancel := context.WithCancelCause(context.Background()) | |
| cancel(nil) | |
| fmt.Println("ctx.Err():", ctx.Err()) | |
| fmt.Println("cause:", context.Cause(ctx)) | |
| // Both are context.Canceled. | |
| } | |
| fmt.Println() | |
| fmt.Println("=== Scenario 4: cause on uncanceled context is nil ===") | |
| { | |
| ctx, cancel := context.WithCancelCause(context.Background()) | |
| defer cancel(nil) | |
| fmt.Println("cause before cancel:", context.Cause(ctx)) | |
| // nil | |
| } | |
| fmt.Println() | |
| fmt.Println("=== Scenario 5: cause on non-cause context returns ctx.Err() ===") | |
| { | |
| ctx, cancel := context.WithCancel(context.Background()) | |
| cancel() | |
| fmt.Println("ctx.Err():", ctx.Err()) | |
| fmt.Println("context.Cause:", context.Cause(ctx)) | |
| // Both are context.Canceled. | |
| } | |
| } | |
| // Output: | |
| // | |
| // === Scenario 1: inventory check fails with connection error === | |
| // returned error: connection refused | |
| // | |
| // === Scenario 2: demonstrating first-cancel-wins === | |
| // cause after first cancel: first reason | |
| // cause after second cancel: first reason | |
| // | |
| // === Scenario 3: cancel(nil) sets cause to context.Canceled === | |
| // ctx.Err(): context canceled | |
| // cause: context canceled | |
| // | |
| // === Scenario 4: cause on uncanceled context is nil === | |
| // cause before cancel: <nil> | |
| // | |
| // === Scenario 5: cause on non-cause context returns ctx.Err() === | |
| // ctx.Err(): context canceled | |
| // context.Cause: context canceled |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // The WithTimeoutCause gotcha: defer cancel() discards the custom cause. | |
| package main | |
| import ( | |
| "context" | |
| "fmt" | |
| "time" | |
| ) | |
| func main() { | |
| fmt.Println("=== Scenario 1: timeout fires (cause is preserved) ===") | |
| { | |
| ctx, cancel := context.WithTimeoutCause( | |
| context.Background(), | |
| 50*time.Millisecond, | |
| fmt.Errorf("order ord-123: 50ms timeout exceeded"), | |
| ) | |
| defer cancel() | |
| // Simulate slow work that exceeds the timeout. | |
| time.Sleep(100 * time.Millisecond) | |
| fmt.Println("ctx.Err():", ctx.Err()) | |
| fmt.Println("cause: ", context.Cause(ctx)) | |
| // ctx.Err() = context deadline exceeded | |
| // cause = order ord-123: 50ms timeout exceeded ✓ | |
| } | |
| fmt.Println() | |
| fmt.Println("=== Scenario 2: function returns before timeout (cause is LOST) ===") | |
| { | |
| ctx, cancel := context.WithTimeoutCause( | |
| context.Background(), | |
| 5*time.Second, | |
| fmt.Errorf("order ord-456: 5s timeout exceeded"), | |
| ) | |
| // Function completes quickly, defer cancel() fires. | |
| cancel() | |
| fmt.Println("ctx.Err():", ctx.Err()) | |
| fmt.Println("cause: ", context.Cause(ctx)) | |
| // ctx.Err() = context canceled (NOT deadline exceeded) | |
| // cause = context canceled (custom cause LOST) | |
| } | |
| fmt.Println() | |
| fmt.Println("=== Scenario 3: showing the return type difference ===") | |
| { | |
| // WithCancelCause returns CancelCauseFunc (takes an error). | |
| _, cancelCause := context.WithCancelCause(context.Background()) | |
| cancelCause(fmt.Errorf("specific reason")) | |
| // WithTimeoutCause returns plain CancelFunc (takes no arguments). | |
| _, cancelTimeout := context.WithTimeoutCause( | |
| context.Background(), | |
| 5*time.Second, | |
| fmt.Errorf("timeout reason"), | |
| ) | |
| cancelTimeout() // Can't pass a cause here. | |
| fmt.Println("WithCancelCause returns CancelCauseFunc: takes an error") | |
| fmt.Println("WithTimeoutCause returns CancelFunc: takes no arguments") | |
| } | |
| } | |
| // Output: | |
| // | |
| // === Scenario 1: timeout fires (cause is preserved) === | |
| // ctx.Err(): context deadline exceeded | |
| // cause: order ord-123: 50ms timeout exceeded | |
| // | |
| // === Scenario 2: function returns before timeout (cause is LOST) === | |
| // ctx.Err(): context canceled | |
| // cause: context canceled | |
| // | |
| // === Scenario 3: showing the return type difference === | |
| // WithCancelCause returns CancelCauseFunc: takes an error | |
| // WithTimeoutCause returns CancelFunc: takes no arguments |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment