etcd-io/etcd · error
bad compare value
Error message
bad compare value
What it means
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.
Source
Thrown at client/v3/compare.go:80
r = pb.Compare_EQUAL
case "!=":
r = pb.Compare_NOT_EQUAL
case ">":
r = pb.Compare_GREATER
case "<":
r = pb.Compare_LESS
default:
panic("Unknown result op")
}
cmp = cmp.Clone()
cmp.ensureCompare()
cmp.c.Result = r
switch cmp.c.Target {
case pb.Compare_VALUE:
val, ok := v.(string)
if !ok {
panic("bad compare value")
}
cmp.c.TargetUnion = &pb.Compare_Value{Value: []byte(val)}
case pb.Compare_VERSION:
cmp.c.TargetUnion = &pb.Compare_Version{Version: mustInt64(v)}
case pb.Compare_CREATE:
cmp.c.TargetUnion = &pb.Compare_CreateRevision{CreateRevision: mustInt64(v)}
case pb.Compare_MOD:
cmp.c.TargetUnion = &pb.Compare_ModRevision{ModRevision: mustInt64(v)}
case pb.Compare_LEASE:
cmp.c.TargetUnion = &pb.Compare_Lease{Lease: mustInt64orLeaseID(v)}
default:
panic("Unknown compare type")
}
return cmp
}
func Value(key string) Cmp {
return Cmp{c: &pb.Compare{Key: []byte(key), Target: pb.Compare_VALUE}}View on GitHub (pinned to f744d457f4)
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.
Example fix
// before
cmp := clientv3.Compare(clientv3.CompareValue("/job/count"), "=", 3) // panics: bad compare value
// after — if the stored value is the string "3"
cmp := clientv3.Compare(clientv3.CompareValue("/job/count"), "=", strconv.Itoa(3))
// or, if you actually want version/revision semantics:
cmp := clientv3.Compare(clientv3.CompareVersion("/job/count"), "=", 3) Defensive patterns
Strategy: type-guard
Type guard
// VALUE-target comparisons require a string:
func assertString(v any) (string, error) {
s, ok := v.(string)
if !ok {
return "", fmt.Errorf("compare value must be string, got %T", v)
}
return s, nil
} Try / catch
defer func() {
if r := recover(); r != nil {
return fmt.Errorf("Compare panicked (likely non-string value for CompareValue): %v", r)
}
}()
cmp := clientv3.Compare(clientv3.CompareValue(k), "=", v) Prevention
- 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).
When it happens
Trigger: 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.
Common situations: 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).
Related errors
- Unknown result op
- bad value %v of type %T
- unsupported stm
- unexpected revision = 0. Calling SyncUpdates before SyncBase
- `WithPrefix` and `WithFromKey` cannot be set at the same tim
AI-assisted analysis of etcd-io/etcd@f744d457f4 (2026-08-15).
Data as JSON: /api/errors/71228fb20c40818b.
Report an issue: GitHub.