etcd-io/etcd · error

cannot call If twice!

Error message

cannot call If twice!

What it means

The txn builder panics with 'cannot call If twice!' when If() is called two or more times on the same clientv3 Txn. etcd transactions follow a strict If/Then/Else grammar where each section may be declared once, in order; the txn struct tracks flags (cif/cthen/celse) and fails fast on grammar violations. The panic is thrown under the txn's mutex before any RPC is sent.

Source

Thrown at client/v3/txn.go:78

	cthen bool
	celse bool

	isWrite bool

	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

View on GitHub (pinned to f744d457f4)

Solutions

  1. Merge all comparisons into a single If call: If(c1, c2, ...) — conditions are ANDed
  2. If conditions are assembled incrementally, collect []clientv3.Cmp first and call If(cs...) once at the end
  3. Restructure helpers to return []Cmp rather than mutating the Txn
  4. Guard shared Txn construction behind one function so the grammar is enforced in one place

Example fix

// before
t := cli.Txn(ctx).If(clientv3.Compare(clientv3.Value("k"), ">", "a"))
t = t.If(clientv3.Compare(clientv3.Version("k"), ">", 0)) // panics

// after
t := cli.Txn(ctx).If(
	clientv3.Compare(clientv3.Value("k"), ">", "a"),
	clientv3.Compare(clientv3.Version("k"), ">", 0),
)
Defensive patterns

Strategy: validation

Validate before calling

// Collect conditions first, call If exactly once:
var cmps []clientv3.Cmp
cmps = append(cmps, clientv3.Compare(clientv3.Version("k"), ">", 0))
cmps = append(cmps, clientv3.Compare(clientv3.Value("k"), "=", "v"))
t := cli.Txn(ctx).If(cmps...) // single If, conditions ANDed

Try / catch

// Wrap composite Txn construction so grammar panics become errors
func buildTxn(ctx context.Context, cli *clientv3.Client, cmps []clientv3.Cmp, thenOps, elseOps []clientv3.Op) (t clientv3.Txn, err error) {
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("txn construction failed: %v", r)
		}
	}()
	return cli.Txn(ctx).If(cmps...).Then(thenOps...).Else(elseOps...), nil
}

Prevention

When it happens

Trigger: Calling t.If(c1).If(c2) on the same Txn, or invoking If again because a helper function takes the Txn and adds conditions. Building conditions in a loop that calls If per iteration instead of passing all Cmps in one call.

Common situations: Accumulating comparison conditions across helper functions or layers (middleware adding conditions); porting code from a fluent builder that allowed repeated calls; refactors that split one If call into several without merging the condition slices.

Related errors


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