Introduction
When a context is cancelled, ctx.Err() tells you why. Go 1.20 added context.Cause for custom error causes, and Go 1.21 added context.AfterFunc for cleanup callbacks. These features let you build nuanced error handling around cancellation.
Key Concepts
- context.Canceled: The error returned when a context is explicitly cancelled via
cancel(). - context.DeadlineExceeded: The error returned when a context's timeout or deadline has passed.
- context.Cause (Go 1.20+): Returns the custom error passed to
CancelCauseFunc, providing richer cancellation reasons.
Real World Context
In an API gateway, distinguishing between Canceled (the client disconnected) and DeadlineExceeded (the backend took too long) determines the HTTP response code: 499 Client Closed Request vs. 504 Gateway Timeout. Custom causes via WithCancelCause can carry even more specific reasons.
Deep Dive
ctx.Err()
Returns the reason the context was cancelled:
context.Canceled: Explicitly cancelled viacancel().context.DeadlineExceeded: Timeout or deadline passed.
goif ctx.Err() == context.Canceled { log.Println("Request was cancelled by client") } else if ctx.Err() == context.DeadlineExceeded { log.Println("Request timed out") }
Context Cause (Go 1.20+)
You can set a custom cause when cancelling:
goctx, cancel := context.WithCancelCause(parent) // Later cancel(errors.New("user cancelled")) // Check cause cause := context.Cause(ctx)
AfterFunc (Go 1.21+)
Register a function to run when context is done:
gostop := context.AfterFunc(ctx, func() { cleanup() }) // Call stop() to cancel the callback if context is not yet done
Common Pitfalls
- Checking ctx.Err() before ctx.Done() —
ctx.Err()returns nil until the context is actually cancelled. Check<-ctx.Done()first, then inspectctx.Err(). - Ignoring the cause in error messages — When using
WithCancelCause, always checkcontext.Cause(ctx)for the most specific error, not justctx.Err().
Best Practices
- Use
WithCancelCausefor debuggable cancellation — Custom causes make it easier to trace why a context was cancelled in logs and error messages. - Use
AfterFuncfor cleanup — It replaces manual goroutine-plus-select patterns for running cleanup when a context expires.
Summary
ctx.Err()returnscontext.Canceledorcontext.DeadlineExceeded.context.Cause(Go 1.20+) provides custom cancellation reasons.context.AfterFunc(Go 1.21+) registers cleanup callbacks on context cancellation.- Always check
ctx.Done()before inspectingctx.Err().
Code Examples
go
func handleRequest(ctx context.Context) error {
result, err := doWork(ctx)
if err != nil {
if ctx.Err() == context.Canceled {
return errors.New("client disconnected")
}
if ctx.Err() == context.DeadlineExceeded {
return errors.New("request timeout")
}
return err
}
return nil
}