etcd-io/etcd · error

bad value %v of type %T

Error message

bad value %v of type %T

What it means

mustInt64 is the coercion helper behind CompareVersion, CompareCreated, and CompareModified comparisons. It accepts only the exact dynamic types int64, int (and LeaseID via mustInt64orLeaseID for lease targets); every other type panics with "bad value %v of type %T". The panic message deliberately includes the offending value and its Go type so the bad call site is obvious.

Source

Thrown at client/v3/compare.go:177

}

// WithPrefix sets the comparison to scan all keys prefixed by the key.
func (cmp Cmp) WithPrefix() Cmp {
	cmp = cmp.Clone()
	cmp.ensureCompare()
	cmp.c.RangeEnd = getPrefix(cmp.c.GetKey())
	return cmp
}

// mustInt64 panics if val isn't an int or int64. It returns an int64 otherwise.
func mustInt64(val any) int64 {
	if v, ok := val.(int64); ok {
		return v
	}
	if v, ok := val.(int); ok {
		return int64(v)
	}
	panic(fmt.Sprintf("bad value %v of type %T", val, val))
}

// mustInt64orLeaseID panics if val isn't a LeaseID, int or int64. It returns an
// int64 otherwise.
func mustInt64orLeaseID(val any) int64 {
	if v, ok := val.(LeaseID); ok {
		return int64(v)
	}
	return mustInt64(val)
}

func cloneCompare(c *pb.Compare) *pb.Compare {
	if c == nil {
		return nil
	}
	return proto.Clone(c).(*pb.Compare)
}

View on GitHub (pinned to f744d457f4)

Solutions

  1. Cast to int64 at the call site: clientv3.Compare(clientv3.CompareVersion(k), "=", int64(myUint64)).
  2. For JSON-derived numbers, convert float64 via int64(f) after range checking.
  3. For lease comparisons, pass a clientv3.LeaseID directly (mustInt64orLeaseID accepts it) rather than a raw integer type.

Example fix

// before
rev := resp.Header.Revision // int64 is fine, but:
cmp := clientv3.Compare(clientv3.CompareVersion(k), "=", uint64(resp.Count)) // panics: bad value N of type uint64

// after
cmp := clientv3.Compare(clientv3.CompareVersion(k), "=", int64(resp.Count))
Defensive patterns

Strategy: type-guard

Type guard

func toInt64(v any) (int64, error) {
	switch n := v.(type) {
	case int64:
		return n, nil
	case int:
		return int64(n), nil
	case uint64:
		return int64(n), nil // explicit widening decision at your boundary
	default:
		return 0, fmt.Errorf("unsupported numeric type %T", v)
	}
}

Try / catch

defer func() {
    if r := recover(); r != nil {
        return fmt.Errorf("Compare panicked (bad numeric value): %v", r)
    }
}()

Prevention

When it happens

Trigger: clientv3.Compare(clientv3.CompareVersion(k), "=", int32(3)), uint64(3), "3", or a JSON-decoded float64. Also passing a typed alias like type MyInt uint16. Note int on 64-bit platforms works, but narrower/wider widths (int8..int32, uint*, float*) do not.

Common situations: Decoding JSON into map[string]interface{} where numbers become float64; using uint64 because etcd revisions/leases are documented as uint64 on the wire; protobuf-generated fields of type int32; version counters stored as custom integer types.

Related errors


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