Frontend Labs
⋯

18. context: cancellation, deadlines, and request-scoped values

18.1 Why context exists

A single incoming HTTP request might fan out to a database query, two downstream API calls, and a cache lookup. If the client disconnects, or a deadline expires, every one of those in-flight operations needs to hear about it now — not run to completion pointlessly. context.Context is the value threaded through every layer of a call stack to carry exactly that signal.

ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel() // ALWAYS defer cancel, even if you expect the timeout to fire first

req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
	return err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
	if errors.Is(err, context.DeadlineExceeded) {
		log.Println("request timed out")
	}
	return err
}
defer resp.Body.Close()
return nil

18.2 The four context constructors

Constructor Cancels when
context.Background() Never — the root of every context tree
context.WithCancel(parent) You call the returned cancel()
context.WithTimeout(parent, d) Duration d elapses, or manual cancel
context.WithDeadline(parent, t) Wall-clock time t is reached

Cancellation propagates downward only: cancelling a parent cancels every context derived from it; a child can never cancel its parent.

18.3 Respecting cancellation in your own code

Context does nothing by itself — code has to actually check ctx.Done():

func worker(ctx context.Context, jobs <-chan Job) {
	for {
		select {
		case <-ctx.Done():
			return // stop working, ctx.Err() explains why
		case job, ok := <-jobs:
			if !ok {
				return
			}
			process(job)
		}
	}
}

18.4 Context values — use sparingly

context.WithValue carries request-scoped metadata (a request ID, an authenticated user) across API boundaries that can't take an explicit parameter. It is not a substitute for passing real parameters — if a function needs a value to do its job, put it in the signature; reserve context values for cross-cutting concerns like tracing IDs.

type requestIDKey struct{}

func withRequestID(ctx context.Context, id string) context.Context {
	return context.WithValue(ctx, requestIDKey{}, id)
}

func requestIDFrom(ctx context.Context) string {
	if id, ok := ctx.Value(requestIDKey{}).(string); ok {
		return id
	}
	return ""
}

Using an unexported struct type as the key (instead of a string) prevents collisions with context keys defined in other packages.

18.5 Real-world example

Week 2's concurrent URL checker cancels all remaining checks if the total job takes longer than 30 seconds — that's context.WithTimeout wrapping the whole batch, passed down into every worker's individual HTTP call, so one shared deadline governs a fan-out of N concurrent requests without any manual timer bookkeeping.

Applied in the series: context cancels dials and probes in the TCP/IP guide's dialcheck and stops streaming handlers on disconnect in the HTTPS guide's SSE lab.


Generated from real-life-golang-guide.md · 8 Oct 2026 16:36