etcd-io/etcd · error

unexpected sort in put

Error message

unexpected sort in put

What it means

OpPut panics with 'unexpected sort in put' when a sort option (WithSort) is applied to a put operation. etcd's clientv3 Op constructors fail fast: options that only make sense for range (Get) queries are rejected at construction time rather than silently ignored. The panic happens synchronously inside OpPut, before any RPC is issued.

Source

Thrown at client/v3/op.go:312

	case ret.createdNotify:
		panic("unexpected createdNotify in delete")
	}
	return ret
}

// OpPut returns "put" operation based on given key-value and operation options.
func OpPut(key, val string, opts ...OpOption) Op {
	ret := Op{t: tPut, key: []byte(key), val: []byte(val)}
	ret.applyOpts(opts)
	switch {
	case ret.end != nil:
		panic("unexpected range in put")
	case ret.limit != 0:
		panic("unexpected limit in put")
	case ret.rev != 0:
		panic("unexpected revision in put")
	case ret.sort != nil:
		panic("unexpected sort in put")
	case ret.serializable:
		panic("unexpected serializable in put")
	case ret.countOnly:
		panic("unexpected countOnly in put")
	case ret.minModRev != 0, ret.maxModRev != 0:
		panic("unexpected mod revision filter in put")
	case ret.minCreateRev != 0, ret.maxCreateRev != 0:
		panic("unexpected create revision filter in put")
	case ret.filterDelete, ret.filterPut:
		panic("unexpected filter in put")
	case ret.createdNotify:
		panic("unexpected createdNotify in put")
	}
	return ret
}

// OpTxn returns "txn" operation based on given transaction conditions.
func OpTxn(cmps []Cmp, thenOps []Op, elseOps []Op) Op {

View on GitHub (pinned to f744d457f4)

Solutions

  1. Remove the WithSort* option from the OpPut call; sorting is meaningless for writes
  2. If the options come from a shared helper, split the option set: keep sort/limit/serializable options only on the OpGet path
  3. Audit the call site for other range-only options (WithLimit, WithRev, WithRange, WithSerializable, WithCountOnly, WithPrefix filters) since the same panic family guards them
  4. Wrap Op construction in a recover() only if you must tolerate third-party option lists

Example fix

// before
op := clientv3.OpPut("foo", "bar", clientv3.WithSort(clientv3.SortByKey, clientv3.SortAscend))

// after
op := clientv3.OpPut("foo", "bar")
Defensive patterns

Strategy: validation

Validate before calling

// Only put-valid options may reach OpPut: WithLease, WithPrevKV, WithIgnoreValue, WithIgnoreLease.
// WithSort is range-only; validate option lists at build time.
func buildPutOpts(opts []clientv3.OpOption) []clientv3.OpOption {
	// put-valid options have zero effect on sort/limit/rev fields;
	// simplest correct check: construct and recover
	func() (ok bool) {
		defer func() { _ = recover() }()
		clientv3.OpPut("\x00probe", "\x00", opts...)
		return true
	}()
	return opts
}

Try / catch

// Go: panics are recoverable; use sparingly for third-party option lists
func safeOpPut(key, val string, opts ...clientv3.OpOption) (op clientv3.Op, err error) {
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("invalid put options: %v", r)
		}
	}()
	return clientv3.OpPut(key, val, opts...), nil
}

Prevention

When it happens

Trigger: Calling clientv3.OpPut(key, val, clientv3.WithSort(...)) or WithSortBytes/WithSortKey/WithSortRev/WithSortVersion/WithSortCreateRev/WithSortModRev/WithSortValue. Also triggered indirectly by building a Put through clientv3.Txn Then/Else branches with a sort option attached, or by reusing an options slice built for a Range query in a Put.

Common situations: Copy-pasting an options list from a Get call into a Put call; generic helper functions that accept variadic OpOption and forward the same set to both OpGet and OpPut; refactoring a range into a put without removing WithSort.

Related errors


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