etcd-io/etcd · error

unexpected sort in watch

Error message

unexpected sort in watch

What it means

OpWatch panics with 'unexpected sort in watch' when any WithSort* option is applied to a watch. Watches stream events in occurrence order; result sorting only applies to range queries, so OpWatch rejects sort options up front. The panic is thrown during Op construction, before any RPC.

Source

Thrown at client/v3/op.go:347

// OpTxn returns "txn" operation based on given transaction conditions.
func OpTxn(cmps []Cmp, thenOps []Op, elseOps []Op) Op {
	clonedCmps := make([]Cmp, len(cmps))
	for i := range cmps {
		clonedCmps[i] = cmps[i].Clone()
	}
	return Op{t: tTxn, cmps: clonedCmps, thenOps: thenOps, elseOps: elseOps}
}

func OpWatch(key string, opts ...OpOption) Op {
	ret := Op{t: tRange, key: []byte(key)}
	ret.applyOpts(opts)
	switch {
	case ret.leaseID != 0:
		panic("unexpected lease in watch")
	case ret.limit != 0:
		panic("unexpected limit in watch")
	case ret.sort != nil:
		panic("unexpected sort in watch")
	case ret.serializable:
		panic("unexpected serializable in watch")
	case ret.countOnly:
		panic("unexpected countOnly in watch")
	case ret.minModRev != 0, ret.maxModRev != 0:
		panic("unexpected mod revision filter in watch")
	case ret.minCreateRev != 0, ret.maxCreateRev != 0:
		panic("unexpected create revision filter in watch")
	}
	return ret
}

func (op *Op) applyOpts(opts []OpOption) {
	for _, opt := range opts {
		opt(op)
	}
}

View on GitHub (pinned to f744d457f4)

Solutions

  1. Remove the WithSort* option from the watch call
  2. Apply sorting client-side to the initial Get used to seed state; rely on event order for the watch
  3. Maintain separate builders: scan options (sort/limit/serializable) vs watch options (WithPrefix, WithPrevKV, WithProgressNotify, WithCreatedNotify, WithFilter*, WithRev, WithFragment)
  4. Write a unit test that constructs all watch ops used by your app to catch option leaks in CI

Example fix

// before
op := clientv3.OpWatch("k", clientv3.WithSort(clientv3.SortByModRevision, clientv3.SortAscend))

// after
seed := clientv3.OpGet("k", clientv3.WithSort(clientv3.SortByModRevision, clientv3.SortAscend))
wop := clientv3.OpWatch("k")
Defensive patterns

Strategy: validation

Validate before calling

// Sort on the seeding read; sort watch events client-side if needed.
seed := clientv3.OpGet("k", clientv3.WithSort(clientv3.SortByModRevision, clientv3.SortAscend))
wop := clientv3.OpWatch("k")

Prevention

When it happens

Trigger: Calling clientv3.OpWatch(key, clientv3.WithSort(...)) or a WithSort* variant; passing an option list assembled for an ordered scan (e.g. WithSort(SortByCreateRevision) for history listing) to a watch on the same key.

Common situations: UI backends that list keys sorted and then subscribe for updates, sharing one options builder; copy-paste from a Get; upgrading code from polling-with-sort to watching without removing sort options.

Related errors


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