{"record":{"id":"33b128ed2d78a5ff","repo":"etcd-io/etcd","slug":"unexpected-sort-in-watch","errorCode":null,"errorMessage":"unexpected sort in watch","messagePattern":"unexpected sort in watch","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"client/v3/op.go","lineNumber":347,"sourceCode":"// OpTxn returns \"txn\" operation based on given transaction conditions.\nfunc OpTxn(cmps []Cmp, thenOps []Op, elseOps []Op) Op {\n\tclonedCmps := make([]Cmp, len(cmps))\n\tfor i := range cmps {\n\t\tclonedCmps[i] = cmps[i].Clone()\n\t}\n\treturn Op{t: tTxn, cmps: clonedCmps, thenOps: thenOps, elseOps: elseOps}\n}\n\nfunc OpWatch(key string, opts ...OpOption) Op {\n\tret := Op{t: tRange, key: []byte(key)}\n\tret.applyOpts(opts)\n\tswitch {\n\tcase ret.leaseID != 0:\n\t\tpanic(\"unexpected lease in watch\")\n\tcase ret.limit != 0:\n\t\tpanic(\"unexpected limit in watch\")\n\tcase ret.sort != nil:\n\t\tpanic(\"unexpected sort in watch\")\n\tcase ret.serializable:\n\t\tpanic(\"unexpected serializable in watch\")\n\tcase ret.countOnly:\n\t\tpanic(\"unexpected countOnly in watch\")\n\tcase ret.minModRev != 0, ret.maxModRev != 0:\n\t\tpanic(\"unexpected mod revision filter in watch\")\n\tcase ret.minCreateRev != 0, ret.maxCreateRev != 0:\n\t\tpanic(\"unexpected create revision filter in watch\")\n\t}\n\treturn ret\n}\n\nfunc (op *Op) applyOpts(opts []OpOption) {\n\tfor _, opt := range opts {\n\t\topt(op)\n\t}\n}\n","sourceCodeStart":329,"sourceCodeEnd":365,"githubUrl":"https://github.com/etcd-io/etcd/blob/f744d457f484e9f748a0700b48ef96dcf792df33/client/v3/op.go#L329-L365","documentation":"OpWatch panics with 'unexpected sort in watch' when any WithSort* option is applied to a watch. Watches stream events in occurrence order; result sorting only applies to range queries, so OpWatch rejects sort options up front. The panic is thrown during Op construction, before any RPC.","triggerScenarios":"Calling clientv3.OpWatch(key, clientv3.WithSort(...)) or a WithSort* variant; passing an option list assembled for an ordered scan (e.g. WithSort(SortByCreateRevision) for history listing) to a watch on the same key.","commonSituations":"UI backends that list keys sorted and then subscribe for updates, sharing one options builder; copy-paste from a Get; upgrading code from polling-with-sort to watching without removing sort options.","solutions":["Remove the WithSort* option from the watch call","Apply sorting client-side to the initial Get used to seed state; rely on event order for the watch","Maintain separate builders: scan options (sort/limit/serializable) vs watch options (WithPrefix, WithPrevKV, WithProgressNotify, WithCreatedNotify, WithFilter*, WithRev, WithFragment)","Write a unit test that constructs all watch ops used by your app to catch option leaks in CI"],"exampleFix":"// before\nop := clientv3.OpWatch(\"k\", clientv3.WithSort(clientv3.SortByModRevision, clientv3.SortAscend))\n\n// after\nseed := clientv3.OpGet(\"k\", clientv3.WithSort(clientv3.SortByModRevision, clientv3.SortAscend))\nwop := clientv3.OpWatch(\"k\")","handlingStrategy":"validation","validationCode":"// Sort on the seeding read; sort watch events client-side if needed.\nseed := clientv3.OpGet(\"k\", clientv3.WithSort(clientv3.SortByModRevision, clientv3.SortAscend))\nwop := clientv3.OpWatch(\"k\")","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Only reads accept WithSort","Rely on event order for watches","Separate scan builders from watch builders"],"tags":["etcd","clientv3","panic","api-misuse","watch","sort"],"backgroundTag":null,"analyzedSha":"f744d457f484e9f748a0700b48ef96dcf792df33","analyzedAt":"2026-08-15T09:39:50.079Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}