{"record":{"id":"71228fb20c40818b","repo":"etcd-io/etcd","slug":"bad-compare-value","errorCode":null,"errorMessage":"bad compare value","messagePattern":"bad compare value","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"client/v3/compare.go","lineNumber":80,"sourceCode":"\t\tr = pb.Compare_EQUAL\n\tcase \"!=\":\n\t\tr = pb.Compare_NOT_EQUAL\n\tcase \">\":\n\t\tr = pb.Compare_GREATER\n\tcase \"<\":\n\t\tr = pb.Compare_LESS\n\tdefault:\n\t\tpanic(\"Unknown result op\")\n\t}\n\n\tcmp = cmp.Clone()\n\tcmp.ensureCompare()\n\tcmp.c.Result = r\n\tswitch cmp.c.Target {\n\tcase pb.Compare_VALUE:\n\t\tval, ok := v.(string)\n\t\tif !ok {\n\t\t\tpanic(\"bad compare value\")\n\t\t}\n\t\tcmp.c.TargetUnion = &pb.Compare_Value{Value: []byte(val)}\n\tcase pb.Compare_VERSION:\n\t\tcmp.c.TargetUnion = &pb.Compare_Version{Version: mustInt64(v)}\n\tcase pb.Compare_CREATE:\n\t\tcmp.c.TargetUnion = &pb.Compare_CreateRevision{CreateRevision: mustInt64(v)}\n\tcase pb.Compare_MOD:\n\t\tcmp.c.TargetUnion = &pb.Compare_ModRevision{ModRevision: mustInt64(v)}\n\tcase pb.Compare_LEASE:\n\t\tcmp.c.TargetUnion = &pb.Compare_Lease{Lease: mustInt64orLeaseID(v)}\n\tdefault:\n\t\tpanic(\"Unknown compare type\")\n\t}\n\treturn cmp\n}\n\nfunc Value(key string) Cmp {\n\treturn Cmp{c: &pb.Compare{Key: []byte(key), Target: pb.Compare_VALUE}}","sourceCodeStart":62,"sourceCodeEnd":98,"githubUrl":"https://github.com/etcd-io/etcd/blob/f744d457f484e9f748a0700b48ef96dcf792df33/client/v3/compare.go#L62-L98","documentation":"When a clientv3.Cmp was created with a VALUE target (CompareValue), Compare() requires the comparison value v to be a Go string, because the value is converted to []byte and stored in a pb.Compare_Value. Passing any other type (int, []byte, fmt.Stringer, etc.) fails the v.(string) type assertion and panics with \"bad compare value\". This is a strict API contract: value targets compare string payloads only.","triggerScenarios":"clientv3.Compare(clientv3.CompareValue(\"k\"), \"=\", 42) or passing []byte(\"val\"), a custom type, or a value read from an interface{} without conversion. CompareVersion/CompareCreated/CompareModified targets accept int/int64, so mixing up targets triggers this.","commonSituations":"Storing numeric counters and reusing the same call pattern as CompareVersion (which takes int64); passing []byte because keys/values elsewhere in clientv3 are []byte; deserializing values from JSON (interface{} holds float64, not string).","solutions":["Convert the value to string before calling: clientv3.Compare(clientv3.CompareValue(k), \"=\", string(myBytes)) or strconv.FormatInt(n, 10).","If you meant to compare a revision or counter, use the right target: CompareVersion / CompareCreated / CompareModified, which accept int or int64.","For values decoded from JSON, convert float64 to string explicitly rather than passing the interface{} directly."],"exampleFix":"// before\ncmp := clientv3.Compare(clientv3.CompareValue(\"/job/count\"), \"=\", 3) // panics: bad compare value\n\n// after — if the stored value is the string \"3\"\ncmp := clientv3.Compare(clientv3.CompareValue(\"/job/count\"), \"=\", strconv.Itoa(3))\n// or, if you actually want version/revision semantics:\ncmp := clientv3.Compare(clientv3.CompareVersion(\"/job/count\"), \"=\", 3)","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"// VALUE-target comparisons require a string:\nfunc assertString(v any) (string, error) {\n\ts, ok := v.(string)\n\tif !ok {\n\t\treturn \"\", fmt.Errorf(\"compare value must be string, got %T\", v)\n\t}\n\treturn s, nil\n}","tryCatchPattern":"defer func() {\n    if r := recover(); r != nil {\n        return fmt.Errorf(\"Compare panicked (likely non-string value for CompareValue): %v\", r)\n    }\n}()\ncmp := clientv3.Compare(clientv3.CompareValue(k), \"=\", v)","preventionTips":["Pair each CompareTarget with its value type in your head: CompareValue -> string; CompareVersion/Created/Modified -> int or int64; lease -> clientv3.LeaseID.","Convert at the boundary: string(b) for []byte, strconv.Itoa/FormatInt for numbers.","Be extra careful with interface{} payloads from JSON decoding (numbers arrive as float64)."],"tags":["go","etcd","clientv3","txn","compare","panic","type-mismatch"],"backgroundTag":null,"analyzedSha":"f744d457f484e9f748a0700b48ef96dcf792df33","analyzedAt":"2026-08-15T09:39:50.079Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}