Common Go Context Mistakes

The context mistakes Go developers make most often, from lost cancel functions and nil contexts to storing contexts in structs, with the warnings and fixes for each.

Most context bugs do not crash. Work keeps running after it should have stopped, timeouts never fire, or goroutines pile up slowly. This page lists the mistakes that cause those problems and how to spot each one.

Discarding the cancel Function

ctx, _ := context.WithTimeout(parent, time.Second)

go vet reports:

the cancel function returned by context.WithTimeout should be called, not discarded, to avoid a context leak

Always keep it and defer cancel().

Calling cancel Too Early

func fetch(parent context.Context) (*http.Response, error) {
	ctx, cancel := context.WithTimeout(parent, 5*time.Second)
	defer cancel()
	req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
	return http.DefaultClient.Do(req) // body read later by caller
}

defer cancel() runs when fetch returns, which cancels the context while the caller is still reading resp.Body. Reads then fail with context canceled. Either read the body inside the function, or let the caller own the context and the timeout.

Using context.Background Deep Inside Code

func (s *Service) Charge(orderID int) error {
	ctx := context.Background() // cut off from the request
	return s.payments.Charge(ctx, orderID)
}

The payment call can no longer be cancelled by the request or its deadline. Accept ctx as the first parameter and pass it through. Reserve Background for main, tests and the start of background jobs.

Passing nil as a Context

f(nil, ...) compiles, then panics with invalid memory address or nil pointer dereference when f calls ctx.Done(). staticcheck warns with SA1012: do not pass a nil Context, even if a function permits it; pass context.TODO if you are unsure about which Context to use.

Storing a Context in a Struct

type Worker struct {
	ctx context.Context // belongs to whichever request created the worker
}

The worker keeps a context that may already be cancelled, or one that never will be. Pass ctx into each method call instead. The context package documentation states this directly: do not store contexts inside a struct type.

Ignoring ctx in Your Own Loops

func process(ctx context.Context, items []Item) error {
	for _, it := range items {
		heavyWork(it) // never checks ctx
	}
	return nil
}

Accepting ctx does nothing by itself. Check ctx.Err() between items, or pass ctx into heavyWork:

package main

import (
	"context"
	"fmt"
	"time"
)

func process(ctx context.Context, items []int) (int, error) {
	done := 0
	for range items {
		if err := ctx.Err(); err != nil {
			return done, err
		}
		time.Sleep(10 * time.Millisecond)
		done++
	}
	return done, nil
}

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 35*time.Millisecond)
	defer cancel()
	n, err := process(ctx, make([]int, 100))
	fmt.Println("processed some, not all:", n > 0 && n < 100, err)
}

This prints processed some, not all: true context deadline exceeded.

Using Context Values for Function Inputs

orderID := ctx.Value("orderID").(int)

Two problems: the key is a plain string, and the type assertion panics with interface conversion: interface {} is nil, not int when the value is missing. Pass required inputs as parameters. Use typed keys and the two-value assertion for genuine metadata.

Giving Background Goroutines the Request Context

go sendWelcomeEmail(r.Context(), user)

The request context is cancelled as soon as the handler returns, so the email is almost always cancelled. Use context.WithoutCancel(r.Context()) with a new timeout, or a context from a long-lived background worker.

Treating context canceled as a Server Error

Logging every context canceled at error level makes client disconnects look like outages. Filter with errors.Is(err, context.Canceled) and log those at a lower level.

Most of these compile cleanly:

Only the discarded cancel function is caught by go vet. Run staticcheck as well, and review code for context.Background() outside main and tests, which catches most of the rest.

A short checklist:

ctx is the first parameter, it is passed on to every call that accepts one, every With... has a defer cancel() in the function that owns the work, no context is stored in a struct, and loops check ctx.Err().

Summary

The common context mistakes are discarding or calling cancel at the wrong time, creating Background contexts deep in code, passing nil, storing contexts in structs, accepting ctx but never checking it, using values for required inputs, handing request contexts to background goroutines, and treating cancellations as failures. go vet and staticcheck catch some, and the checklist above covers the rest.