Context cancelation

March 20, 2025

As already mentioned, the context.Context type serves two purposes: It manages cancelation signals, and it allows passing request-scoped data.

In my opinion, these two purposes are distinct enough that they probably should be handled by two separate mechanisms. But alas, that’s not what we have.

However, I think it’s still useful to think of the two uses as distinct. Most often (at least in a well-designed application), the primary purpose of a context is the propegation of cancelation signals. And that’s what we’re talking about today…

Overview

A Context may be canceled to indicate that work done on its behalf should stop. A Context with a deadline is canceled after the deadline passes. When a Context is canceled, all Contexts derived from it are also canceled.

So when a context is canceled, that’s an indication that work should be canceled, as it will no longer be of any use to the caller. That cancelation can be triggered in one of two ways:

  1. After a timeout.
  2. When the context is explicitly canceled.

The WithCancel, WithDeadline, and WithTimeout functions take a Context (the parent) and return a derived Context (the child) and a CancelFunc. Calling the CancelFunc directly cancels the child and its children, removes the parent’s reference to the child, and stops any associated timers. Failing to call the CancelFunc leaks the child and its children until the parent is canceled. The go vet tool checks that CancelFuncs are used on all control-flow paths.

The context gives us three basic ways to construct cancelable contexts (there are a few derivatives we’ll discuss later.):

  • WithDeadline and WithTimeout allow specifying a time-based cancelation.
  • WithCancel returns a CancelFunc that can be used to explicitly cancel a function.

I’ll show an examples, but we’ll save a detailed discussion for each functoin until later.

func doSomething(ctx context.Context) error {
	ctx, cancel := context.WithTimeout(ctx, 10 * time.Second)
	defer cancel()

	if err := doSomethingElse(ctx); err != nil {
		return err
	}

	if err := doYetMoreThings(ctx); err != nil {
		return err
	}

	return nil
}

In tihs example, we take an existing context value, passed into our doSomething function, then from that derive a new context from it with a 10-second timeout, and a CancelFunc function (cancel). Then we pass that new context to doSomethingElse().

In this configuration, both doSomethingElse and doYetMoreThings should stop processing, and return early, if any of the following conditions are met:

  • The original context (the one passed to doSomething) is canceled,
  • 10 seconds pass,
  • the cancel() function is called explicitly.

If doSomethingElse spends 8 seconds doing work, then doYetMoreThings will have only 2 seconds remaining to do its work before the context will be canceled.

Note that in this example, the explicit call to cancel() is only invoked when doSomething() returns (by way of calling it with the defer keyword). This means that doSomethingElse and doYetMoreThings will never be canceled for this reason. But even in such cases, you should always call the returned CancelFunc function, to clean up any resources used by the context.


Share this

Direct to your inbox, daily. I respect your privacy .

Unsure? Browse the archive .

Related Content


Context plumbing

Monday we looked at a very high-level view of what problems the context package is meant to solve. Namely, to propagate cancelation signales and/or request-scoped values through the call stack of an application. Today we’ll take a brief look at how context “plumbing” works—and answer they why of all those mysterious context.Context function parameters you’ve probably seen, being passed around more than a virus in a daycare. Overview … Incoming requests to a server should create a Context, and outgoing calls to servers should accept a Context.


Concurrent use of contexts

Overview … The same Context may be passed to functions running in different goroutines; Contexts are safe for simultaneous use by multiple goroutines. Contexts are built using layers. Every time you call one of the context.With* functions, the orginal context is wrapped in a new layer. This is the “magic” that makes contexts concurrency safe by design–they’re never mutated once they’re created. ctx := context.Background() // Create an original context ctx, cancel = context.


Context abuse

We’re nearly through the overview of the context package, when we come across this seemlingly straightforward sentence: Overview … Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions. But is that really so straight forward? Let’s consider some examples. Let’s first tackle what probably is straight-forward: For garden variety optional parameters, context is the wrong tool! Do this: // Connect connects to the service using the provided host and port, or the // default host and/or port if omitted.

Get daily content like this in your inbox!

Subscribe