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 deadlines

On Friday we looked at the context.Context interface as a whole. Now let’s go back and look at each method in greater detail. type Context type Context interface { // Deadline returns the time when work done on behalf of this context // should be canceled. Deadline returns ok==false when no deadline is // set. Successive calls to Deadline return the same results. Deadline() (deadline time.Time, ok bool) So there are two general cases here:


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.


Contexts with timeouts

func WithTimeout func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)). Canceling this context releases resources associated with it, so code should call cancel as soon as the operations running in this Context complete: func slowOperationWithTimeout(ctx context.Context) (Result, error) { ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond) defer cancel() // releases resources if slowOperation completes before timeout elapses return slowOperation(ctx) } Not only is WithTimeout conceptually similar to WithDeadline, it literally is WithDeadline, as we can see form the source code:

Get daily content like this in your inbox!

Subscribe