Skip to main content

Context Cancellation in Go: Three Patterns I Use Every Day

1 min read

The problem

When we first adopted Go at work, context.Context felt like boilerplate. Pass it everywhere, check .Done() in loops, done. Then a cascading timeout brought down three services because one context was silently swallowed.

Pattern 1: Always chain with WithTimeout, never WithCancel alone

// Bad — no deadline means the context lives forever
ctx, cancel := context.WithCancel(parentCtx)

// Good — timeout bounds the lifetime
ctx, cancel := context.WithTimeout(parentCtx, 5*time.Second)
defer cancel()

If you don’t set a timeout, you’re just adding ceremony.

Pattern 2: Return the context’s error, not a wrapper

When a function takes a context, its errors should expose whether cancellation caused the failure:

if ctx.Err() != nil {
    return 0, fmt.Errorf("fetch: %w", ctx.Err())
}

Callers can then use errors.Is(err, context.DeadlineExceeded) to decide retry vs abort.

Pattern 3: Don’t store contexts in structs

The standard library’s own http.Request carries a context. That doesn’t mean your struct should. Pass them explicitly to methods that need them.

What I learned

Treat context like a request-scoped value. The moment you stash it in a field, you’ve lost the boundary.