Context causes

April 29, 2025

About a week ago, I talked about the two error types that a context may return: context.Canceled and context.DeadlineExceeded.

But what if you want to convey some other error? Maybe you want to distinguish between a context that was canceled because the server is shutting down due to a SIGINT, versus becasue the request was canceled by the client, for example?

Enter Cause.

func Cause

func Cause(c Context) error

Cause returns a non-nil error explaining why c was canceled. The first cancellation of c or one of its parents sets the cause. If that cancellation happened via a call to CancelCauseFunc(err), then Cause returns err. Otherwise Cause(c) returns the same value as c.Err(). Cause returns nil if c has not been canceled yet.

This was added in Go 1.20, and provides some additional flexibility with error values, without breaking backward compatibility.

Let’s jump ahead a bit, to look at the other half of this function in the documentation:

func WithCancelCause

func WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc)

WithCancelCause behaves like WithCancel but returns a CancelCauseFunc instead of a CancelFunc. Calling cancel with a non-nil error (the “cause”) records that error in ctx; it can then be retrieved using Cause(ctx). Calling cancel with nil sets the cause to Canceled.

Example use:

ctx, cancel := context.WithCancelCause(parent)
cancel(myError)
ctx.Err() // returns context.Canceled
context.Cause(ctx) // returns myError

Now there are some limitations here. Most notably, any third-party code won’t automatically know to check your context for a cause. This means you can’t magically inject unexpected errors that pop up in strange places:

ctx, cancel := context.WithCancelCause(parent)
cancel(io.EOF)
err := callThirdPartyLibrary(ctx) // probably err == context.Canceled, not io.EOF

To find the ‘cause’ error, your code has to explicitly look for it, as in the example above. You can work around this a bit, in the case of third-party code, if you want to:

ctx, cancel := context.WithCancelCause(parent)
cancel(io.EOF)
err := callThirdPartyLibrary(ctx) // probably err == context.Canceled, not io.EOF
if errors.Is(err, context.Canceled) {
	cause := context.Cause(ctx)
	if errors.Is(cause, io.EOF) {
		// Now we know it's
	}
}

You’ll probably never need anything like that. I mention it not as an example of what you should do, but just to be thorough 😉

If you need context.Cause, you’ll typically want it to enhance context handling in your own code, not third-party code:

select {
case <-ctx.Done()
	err := ctx.Err()
	if e := contxt.Cause(ctx); e != nil {
		err = e
	}
	return err
default:
}

Share this

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

Unsure? Browse the archive .

Related Content


Cancelation causes

We’ve seen that we can cancel contexts in a few ways–explicitly, or via timeouts. And that canceling a parent context also cancels all of its children. But in all of these cases we get only two possible error values: context.Canceled or context.DeadlineExceeded. What if you wish to express additional details about a cancelation? This is where the concept of a ‘cause’ comes into play… Overview … The WithCancelCause, WithDeadlineCause, and WithTimeoutCause functions return a CancelCauseFunc, which takes an error and records it as the cancellation cause.


Contexts with deadlines

Today we’re talking about canceling contexts based on time. Arguably, this should have come first, as it’s a little simpler to work with than the explicit cancelation we talked about a week ago. But once again, I’m going in essentially alphabetical order, so it’s what we have… func WithDeadline func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) WithDeadline returns a derived context that points to the parent context but has the deadline adjusted to be no later than d.


Context cancelation

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.

Get daily content like this in your inbox!

Subscribe