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) errorCause 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:
}