Skip to content

Day 040 — context: Timeouts & Cancellation

Month 2 · Week 2 · ⬅ Day 039 · Day 041 ➡ · Journal index

🎯 Learning Objective

Propagate deadlines and cancellation across API boundaries with context.Context: create derived contexts, honor cancellation in blocking code, and detect why a context ended.

📚 Topics

  • context.Background / TODO · the derivation tree
  • WithCancel / WithTimeout / WithDeadline · cancel() discipline
  • ctx.Done(), ctx.Err(), context.Cause
  • errors.Is(err, context.DeadlineExceeded / Canceled)
  • WithValue (sparingly) · context-first convention

📖 Reading / Sources

📝 Notes

  • A Context carries a deadline, a cancellation signal, and request-scoped values across API boundaries and goroutines. By convention it is the first parameter, named ctx, and is never stored in a struct (pass it explicitly) → [[context-first-param]].
  • Start from a root: context.Background() (main/top of request) or context.TODO() (placeholder while plumbing). Derive children:
  • WithCancel(parent) → manual cancel().
  • WithTimeout(parent, d) → auto-cancels after d.
  • WithDeadline(parent, t) → auto-cancels at wall time t.
  • Always call cancel (usually defer cancel()), even on the timeout/deadline forms — it releases the timer and child resources immediately. Leaking cancel is a context (and goroutine) leak vet will warn about → [[always-call-cancel]].
  • Cancellation propagates down the tree: cancelling a parent cancels all descendants; cancelling a child doesn't touch the parent.
  • A context does not stop code by itself — blocking work must select on ctx.Done() (or pass ctx to a context-aware API like http.NewRequestWithContext or db.QueryContext). A bare time.Sleep ignores cancellation → [[honor-ctx-done]].
  • After Done() is closed, ctx.Err() tells you why: context.Canceled or context.DeadlineExceeded. Check with errors.Is(err, context.DeadlineExceeded) since wrappers nest it. context.Cause(ctx) (Go 1.20+) returns a richer cause when set via WithCancelCause.
  • context.WithValue carries request-scoped data (request ID, auth) only — not optional function params. Use an unexported key type to avoid collisions; never pass config through it → [[context-value-sparingly]].

💻 Code Examples

// Per-call deadline that auto-cancels; defer cancel() always.
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()

select {
case res := <-slowWork(ctx):
    use(res)
case <-ctx.Done():
    // ctx.Err() is context.DeadlineExceeded or context.Canceled
    return ctx.Err()
}

Full code: examples/month-02/http-context/main.go · Run: go run ./examples/month-02/http-context

🏋️ Exercises / Practice

Exercise Status Link
Do(ctx, attempts, delay, fn) — cancellable retry/backoff exercises/month-02/week-2/retry

🐛 Mistakes Made

  • Created a WithTimeout context but forgot defer cancel()go vet flagged "lost cancel"; the timer lingered.
  • Expected WithTimeout to kill a time.Sleep — it doesn't; I had to select on ctx.Done() instead.
  • Compared err == context.DeadlineExceeded after wrapping with %w and it failed — switched to errors.Is.

❓ Open Questions

  • When is WithCancelCause / context.Cause worth the extra plumbing over a plain WithCancel?

🧠 Active Recall (answer without looking)

  1. Q: Does passing a ctx with a 50 ms timeout to a function automatically stop its work after 50 ms?

    A No. A context only *signals*; it can't preempt code. The function must select on `ctx.Done()` or call a context-aware API. Otherwise it runs to completion regardless of the deadline.

  2. Q: Why call cancel() even when you used WithTimeout (which auto-cancels)?

    A To release the context's timer and child resources *immediately* when the work finishes early, instead of waiting for the deadline. Skipping it leaks resources until timeout — `defer cancel()` is the idiom.

🪶 Feynman Reflection

A context is a "stop" signal you thread through every call in a request. Parents hand it to children; if a parent calls "stop" (or its timer runs out), everyone downstream hears it on ctx.Done(). But hearing isn't obeying — each piece of blocking work has to actually listen for that signal, otherwise it keeps going. And you always tidy up with cancel() so the alarm clock isn't left ticking.

🕳️ Knowledge Gaps

  • WithCancelCause/Cause patterns and when to surface causes to callers.
  • Propagating context cleanly through deeply layered libraries.

✅ Summary

I can derive timeout/cancel contexts, always defer cancel(), make blocking code honor ctx.Done(), and distinguish cancellation from deadline with errors.Is.

⏭️ Next Steps / Prep for Tomorrow

  • Day 041: log/slog structured logging — and attaching request context to log lines.

Time spent Difficulty Confidence
90 min 🟦🟦🟦⬜⬜ 🟦🟦🟦⬜⬜

Suggested commit: feat(exercises): context timeouts and cancellation (day 040)