etcd-io/etcd · error

cannot call If after Then!

Error message

cannot call If after Then!

What it means

The txn builder panics with 'cannot call If after Then!' when If() is called on a Txn that has already had Then() called. etcd's Txn grammar is strictly ordered If -> Then -> Else; the cthen flag records that Then was declared, and any later If is a programming error that fails fast with a panic. No RPC has been sent when this fires.

Source

Thrown at client/v3/txn.go:82

	cmps []*pb.Compare

	sus []*pb.RequestOp
	fas []*pb.RequestOp

	callOpts []grpc.CallOption
}

func (txn *txn) If(cs ...Cmp) Txn {
	txn.mu.Lock()
	defer txn.mu.Unlock()

	if txn.cif {
		panic("cannot call If twice!")
	}

	if txn.cthen {
		panic("cannot call If after Then!")
	}

	if txn.celse {
		panic("cannot call If after Else!")
	}

	txn.cif = true

	for i := range cs {
		cmp := cs[i].Clone()
		txn.cmps = append(txn.cmps, cmp.GetCompare())
	}

	return txn
}

func (txn *txn) Then(ops ...Op) Txn {
	txn.mu.Lock()

View on GitHub (pinned to f744d457f4)

Solutions

  1. Reorder the calls so all If(...) come before Then(...)/Else(...) on that Txn instance
  2. Build conditions and branches as local slices, then assemble Txn in one place: If(cs...).Then(ops...).Else(ops...)
  3. Make helper functions pure (return Cmp/Op slices) so ordering cannot be violated piecemeal
  4. Add a unit test per composite transaction your code builds to catch ordering bugs in CI

Example fix

// before
t := cli.Txn(ctx).Then(clientv3.OpPut("k", "v"))
t = t.If(clientv3.Compare(clientv3.Version("k"), "=", 0)) // panics

// after
t := cli.Txn(ctx).
	If(clientv3.Compare(clientv3.Version("k"), "=", 0)).
	Then(clientv3.OpPut("k", "v"))
Defensive patterns

Strategy: validation

Validate before calling

// Assemble data first, then the chain in order:
t := cli.Txn(ctx).
	If(cmps...).
	Then(thenOps...).
	Else(elseOps...) // If always before Then/Else

Prevention

When it happens

Trigger: Sequences like cli.Txn(ctx).Then(op).If(cmp), typically caused by calling If after a Then in reordered code, or by a helper that unconditionally adds conditions but is invoked after the caller already built the Then branch.

Common situations: Refactoring that moves condition construction after branch construction; wrapper APIs where the caller sets Then/Else first and a framework hook injects If later; merge/refactor mistakes where lines get shuffled.

Related errors


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