go icon indicating copy to clipboard operation
go copied to clipboard

proposal: context: context.Expired

Open sivchari opened this issue 6 months ago • 6 comments

Proposal Details

Proposal Details

Proposed addition:

func Expired(err error) bool

Motivation

Currently, many codebases should check whether the error is context.Canceled or context.DeadlineExceeded like this.

x, err := doFunc(ctx) // call 
if err != nil {
    if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)
        // handle
    }
    return err
}

As the above case, it's right if developer want to detect any context cancellation, but this handling is redundant. This syntax search shows:

  • 6.5k errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded): https://github.com/search?q=errors.Is%28err%2C+context.Canceled%29+%7C%7C+errors.Is%28err%2C+context.DeadlineExceeded%29&type=code&p=1
  • 508 !errors.Is(err, context.Canceled) || !errors.Is(err, context.DeadlineExceeded): https://github.com/search?q=%21errors.Is%28err%2C+context.Canceled%29+%7C%7C+%21errors.Is%28err%2C+context.DeadlineExceeded%29&type=code&p=1

Of course it's fine that's only redundant, unfortunately, some bugs can be happened since the returning error is different by Cancel and Timeout. These are example that happens bugs.

https://github.com/trinodb/trino-go-client/pull/140 https://github.com/argoproj/argo-cd/pull/22824

These cases tried to consider cancellation correctly, but the difference between the explicit cancellation and timeouted cancellation couldn't be considered. To reduce these subtle incorrect handling, I propose above new API. Of course, we can use Canceled and DeadlineExceeded separately to distinguish these error.

sivchari avatar Apr 29 '25 13:04 sivchari

I think that if we go with this this should be a:

var Expired error = expiredError{}

And then we can define an Is method.

So the use would look like this:

if errors.Is(err, context.Expired) {
    // ....
}

mateusz834 avatar Apr 29 '25 15:04 mateusz834

Is this sufficient though? Now that there's WithCancelCause / WithDeadlineCause / WithTimeoutCause, a context may be canceled without matching either error.

seankhliao avatar Apr 29 '25 16:04 seankhliao

Now that there's WithCancelCause / WithDeadlineCause / WithTimeoutCause, a context may be canceled without matching either error.

Cause errors are only returned through context.Cause(err), and i don't think that we can match the error in any way, as such errors are not wrapped. So that would only work for code using ctx.Err().

mateusz834 avatar Apr 29 '25 16:04 mateusz834

Concerning WithCancelCause / WithDeadlineCause / WithTimeoutCause, like opinion @mateusz834 , I expect to use through ctx.Error or the error wrapped ctx.Error. Cause is for knowing detailed reason why the context is canceled, it's not scope about usage of this proposal.

sivchari avatar Apr 30 '25 14:04 sivchari

For deadline errors, the error deadlineExceededError implements this interface: Timeout() bool , perhaps this kind of error interface must be named in the stdlib errors package?

var terr interface{ Timeout() bool }
if errors.As(err, &terr){
  ...
}

could be

var terr errors.ErrTimeout
if errors.As(err, &terr) && terr.Timeout(){
  ...
}

or perhaps

if errors.IsTimeout(err) { // to simplify
  ...
}

peczenyj avatar Jun 15 '25 12:06 peczenyj

If we were to add anything, I think it would be:

package context

// Error is an error value which matches Canceled and DeadlineExceeded.
//
// To test to see if an error results from a canceled or expired context:
//    if errors.Is(err, context.Error) { ... }
var Error error

I'll note, however, that you can write the motivating example as:

x, err := doFunc(ctx) // call 
if err != nil && ctx.Err() != nil {
  // Possibly err doesn't result from ctx being canceled,
  // but ctx is definitely canceled.
}

neild avatar Jun 16 '25 08:06 neild

I liked @neild idea.

I'll note, however, that you can write the motivating example as:

As you mentioned, if context and error are handled within the same function, this approach should work. However, as shown in the code search results, there are many cases where context is not held within the same scope (e.g., extracted as isContextError), and since comparison using errors.Is is standard as an API for error checking in Go, I believe this proposal is valuable.

sivchari avatar Jul 23 '25 07:07 sivchari

Are there any progress ?

sivchari avatar Nov 21 '25 09:11 sivchari