{"record":{"id":"b64011c3f6b9a724","repo":"go-kratos/kratos","slug":"watch-context-canceled-v","errorCode":null,"errorMessage":"watch context canceled: %v","messagePattern":"watch context canceled: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"contrib/registry/discovery/impl_discover.go","lineNumber":67,"sourceCode":"\t\tcancelCtx:   ctx,\n\t}, nil\n}\n\ntype watcher struct {\n\tresolve *Resolve\n\n\tcancelCtx   context.Context\n\tserviceName string\n}\n\nfunc (w *watcher) Next() ([]*registry.ServiceInstance, error) {\n\tevent := w.resolve.Watch()\n\n\tselect {\n\tcase <-event:\n\t// change event come\n\tcase <-w.cancelCtx.Done():\n\t\treturn nil, fmt.Errorf(\"watch context canceled: %v\", w.cancelCtx.Err())\n\t}\n\n\tctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)\n\tdefer cancel()\n\n\tins, ok := w.resolve.fetch(ctx)\n\tif !ok {\n\t\treturn nil, errors.New(\"Discovery.GetService fetch failed\")\n\t}\n\n\tout := filterInstancesByZone(ins, w.resolve.d.config.Zone)\n\tif len(out) == 0 {\n\t\treturn nil, fmt.Errorf(\"Discovery.GetService(%s) not found\", w.serviceName)\n\t}\n\n\treturn out, nil\n}\n","sourceCodeStart":49,"sourceCodeEnd":85,"githubUrl":"https://github.com/go-kratos/kratos/blob/668db92c2c001e9552594ba5a8aede8456af6d7e/contrib/registry/discovery/impl_discover.go#L49-L85","documentation":"watcher.Next blocks on either a resolve event or the context passed to Discovery.Watch; when that context is canceled or times out, Next returns 'watch context canceled: <cause>'. Expected on shutdown, surprising when a request-scoped context was passed to Watch.","triggerScenarios":"watcher.Next() running while the context given to Discovery.Watch(ctx, ...) gets canceled or exceeds its deadline, so the select takes the cancelCtx.Done() branch.","commonSituations":"Passing a request/handler context into Watch that dies when the handler returns; shutdown ordering that cancels the context before Stop; short-lived consumers with tight deadlines.","solutions":["Pass a long-lived (application lifecycle) context to Watch instead of a request context","Call watcher.Stop() before canceling the context during shutdown","Treat context.Canceled from Next() as a normal stop, not an error to alert on"],"exampleFix":"// before\nw, _ := disc.Watch(reqCtx, \"user-service\") // reqCtx canceled when handler returns\n\n// after\nw, _ := disc.Watch(appCtx, \"user-service\") // long-lived\ngo func() {\n    defer w.Stop()\n    for {\n        if _, err := w.Next(); err != nil {\n            return\n        }\n    }\n}()","handlingStrategy":"validation","validationCode":"if err := ctx.Err(); err != nil {\n    return err // do not enter Next() with an already-canceled context\n}\nins, err := w.Next()","typeGuard":null,"tryCatchPattern":"ins, err := w.Next()\nif err != nil {\n    if errors.Is(ctx.Err(), context.Canceled) || errors.Is(ctx.Err(), context.DeadlineExceeded) {\n        return nil // expected shutdown: stop cleanly\n    }\n    return err\n}","preventionTips":["Pass app-lifecycle contexts to Watch, never request contexts","Call watcher.Stop() before canceling the context","Treat context.Canceled from Next() as normal shutdown"],"tags":["context","watcher","lifecycle","cancellation","go"],"backgroundTag":null,"analyzedSha":"668db92c2c001e9552594ba5a8aede8456af6d7e","analyzedAt":"2026-08-16T02:07:20.704Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}