Skip to content

Instantly share code, notes, and snippets.

@rednafi
Last active February 25, 2026 16:36
Show Gist options
  • Select an option

  • Save rednafi/3d829a0e1aaa6ae6219c02d70168e165 to your computer and use it in GitHub Desktop.

Select an option

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/
// 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
// 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
// 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>
// 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
// 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)
// 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
// 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