{"record":{"id":"12db56d6578d9db3","repo":"dgraph-io/badger","slug":"must-have-caught-a-nil-callback-for-txn-commitwith","errorCode":null,"errorMessage":"Must have caught a nil callback for txn.CommitWith","messagePattern":"Must have caught a nil callback for txn\\.CommitWith","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"txn.go","lineNumber":685,"sourceCode":"\t// If batchSet failed, LSM would not have been updated. So, no need to rollback anything.\n\n\t// TODO: What if some of the txns successfully make it to value log, but others fail.\n\t// Nothing gets updated to LSM, until a restart happens.\n\treturn txnCb()\n}\n\ntype txnCb struct {\n\tcommit func() error\n\tuser   func(error)\n\terr    error\n}\n\nfunc runTxnCallback(cb *txnCb) {\n\tswitch {\n\tcase cb == nil:\n\t\tpanic(\"txn callback is nil\")\n\tcase cb.user == nil:\n\t\tpanic(\"Must have caught a nil callback for txn.CommitWith\")\n\tcase cb.err != nil:\n\t\tcb.user(cb.err)\n\tcase cb.commit != nil:\n\t\terr := cb.commit()\n\t\tcb.user(err)\n\tdefault:\n\t\tcb.user(nil)\n\t}\n}\n\n// CommitWith acts like Commit, but takes a callback, which gets run via a\n// goroutine to avoid blocking this function. The callback is guaranteed to run,\n// so it is safe to increment sync.WaitGroup before calling CommitWith, and\n// decrementing it in the callback; to block until all callbacks are run.\nfunc (txn *Txn) CommitWith(cb func(error)) {\n\tif cb == nil {\n\t\tpanic(\"Nil callback provided to CommitWith\")\n\t}","sourceCodeStart":667,"sourceCodeEnd":703,"githubUrl":"https://github.com/dgraph-io/badger/blob/2a001d466f6b71a917319a1db41f99860e16e269/txn.go#L667-L703","documentation":"This panic guards runTxnCallback against a txnCb whose user callback field is nil. CommitWith is supposed to reject nil callbacks up front, so reaching this panic means the nil check in CommitWith was bypassed or the txnCb was constructed without a user function. It indicates an internal invariant violation, not normal user-facing error handling.","triggerScenarios":"Calling CommitWith with a nil func(error) on a code path where the CommitWith nil-check is skipped (patched/forked code), or a txnCb built with cb.user == nil passed to runTxnCallback.","commonSituations":"Custom forks of Badger; testing harnesses that call runTxnCallback directly; subtle bugs where a nil-typed func value is wrapped (typed nil interface) and slips past an `if cb == nil` check in CommitWith.","solutions":["Always pass a concrete non-nil func(error) to CommitWith — e.g. cb := func(error){}; txn.CommitWith(cb)","Upgrade Badger: current versions make CommitWith's nil check unconditionally hit first","If wrapping callbacks in interfaces, check reflect.ValueOf(cb).IsNil() to catch typed-nil funcs"],"exampleFix":"// before\nvar cb func(error)\ntxn.CommitWith(cb) // nil, panics\n// after\ntxn.CommitWith(func(err error) { if err != nil { log.Error(err) } })","handlingStrategy":"validation","validationCode":"if cb == nil { return errors.New(\"CommitWith requires a non-nil callback\") }\ntxn.CommitWith(cb)","typeGuard":"func isNilFunc(cb func(error)) bool { return cb == nil || reflect.ValueOf(cb).IsNil() }","tryCatchPattern":"defer func(){ if r := recover(); r != nil && strings.Contains(fmt.Sprint(r), \"nil callback\") { /* restore and re-create txn */ } }()","preventionTips":["Always construct the callback inline at the CommitWith call site","Beware typed-nil func values when callbacks come from interfaces"],"tags":["panic","nil-callback","internal-invariant","commit-callback"],"backgroundTag":"nil-callback-panic","analyzedSha":"2a001d466f6b71a917319a1db41f99860e16e269","analyzedAt":"2026-09-05T13:00:02.264Z","contentChangedAt":"2026-09-05T13:00:02.264Z","schemaVersion":2},"datasetVersion":"2026-09-12T17:17:11.597Z"}