{"record":{"id":"8c88594304978fa2","repo":"etcd-io/etcd","slug":"cannot-call-if-twice","errorCode":null,"errorMessage":"cannot call If twice!","messagePattern":"cannot call If twice!","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"client/v3/txn.go","lineNumber":78,"sourceCode":"\tcthen bool\n\tcelse bool\n\n\tisWrite bool\n\n\tcmps []*pb.Compare\n\n\tsus []*pb.RequestOp\n\tfas []*pb.RequestOp\n\n\tcallOpts []grpc.CallOption\n}\n\nfunc (txn *txn) If(cs ...Cmp) Txn {\n\ttxn.mu.Lock()\n\tdefer txn.mu.Unlock()\n\n\tif txn.cif {\n\t\tpanic(\"cannot call If twice!\")\n\t}\n\n\tif txn.cthen {\n\t\tpanic(\"cannot call If after Then!\")\n\t}\n\n\tif txn.celse {\n\t\tpanic(\"cannot call If after Else!\")\n\t}\n\n\ttxn.cif = true\n\n\tfor i := range cs {\n\t\tcmp := cs[i].Clone()\n\t\ttxn.cmps = append(txn.cmps, cmp.GetCompare())\n\t}\n\n\treturn txn","sourceCodeStart":60,"sourceCodeEnd":96,"githubUrl":"https://github.com/etcd-io/etcd/blob/f744d457f484e9f748a0700b48ef96dcf792df33/client/v3/txn.go#L60-L96","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nt := cli.Txn(ctx).If(clientv3.Compare(clientv3.Value(\"k\"), \">\", \"a\"))\nt = t.If(clientv3.Compare(clientv3.Version(\"k\"), \">\", 0)) // panics\n\n// after\nt := cli.Txn(ctx).If(\n\tclientv3.Compare(clientv3.Value(\"k\"), \">\", \"a\"),\n\tclientv3.Compare(clientv3.Version(\"k\"), \">\", 0),\n)","handlingStrategy":"validation","validationCode":"// Collect conditions first, call If exactly once:\nvar cmps []clientv3.Cmp\ncmps = append(cmps, clientv3.Compare(clientv3.Version(\"k\"), \">\", 0))\ncmps = append(cmps, clientv3.Compare(clientv3.Value(\"k\"), \"=\", \"v\"))\nt := cli.Txn(ctx).If(cmps...) // single If, conditions ANDed","typeGuard":null,"tryCatchPattern":"// Wrap composite Txn construction so grammar panics become errors\nfunc buildTxn(ctx context.Context, cli *clientv3.Client, cmps []clientv3.Cmp, thenOps, elseOps []clientv3.Op) (t clientv3.Txn, err error) {\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\terr = fmt.Errorf(\"txn construction failed: %v\", r)\n\t\t}\n\t}()\n\treturn cli.Txn(ctx).If(cmps...).Then(thenOps...).Else(elseOps...), nil\n}","preventionTips":["One If per Txn; pass all Cmps together","Helpers should return []Cmp, not call If","Build the whole chain in a single function"],"tags":["etcd","clientv3","panic","api-misuse","txn","fluent-builder"],"backgroundTag":null,"analyzedSha":"f744d457f484e9f748a0700b48ef96dcf792df33","analyzedAt":"2026-08-15T09:39:50.079Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}