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 txnView on GitHub (pinned to f744d457f4)
Solutions
- Merge all comparisons into a single If call: If(c1, c2, ...) — conditions are ANDed
- If conditions are assembled incrementally, collect []clientv3.Cmp first and call If(cs...) once at the end
- Restructure helpers to return []Cmp rather than mutating the Txn
- 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
- One If per Txn; pass all Cmps together
- Helpers should return []Cmp, not call If
- Build the whole chain in a single function
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
- cannot call Then twice!
- cannot call Else twice!
- cannot call If after Then!
- cannot call If after Else!
- cannot call Then after Else!
AI-assisted analysis of etcd-io/etcd@f744d457f4 (2026-08-15).
Data as JSON: /api/errors/8c88594304978fa2.
Report an issue: GitHub.