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

  1. Convert the value to string before calling: clientv3.Compare(clientv3.CompareValue(k), "=", string(myBytes)) or strconv.FormatInt(n, 10).
  2. If you meant to compare a revision or counter, use the right target: CompareVersion / CompareCreated / CompareModified, which accept int or int64.
  3. 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

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


AI-assisted analysis of etcd-io/etcd@f744d457f4 (2026-08-15). Data as JSON: /api/errors/71228fb20c40818b. Report an issue: GitHub.