dgraph-io/badger · error
Trying to commit a discarded txn
Error message
Trying to commit a discarded txn
What it means
commitPrecheck panics with this error when txn.Commit() (or CommitWith) is called on a transaction that has already been discarded via txn.Discard(). Badger transactions are single-use; once discarded they can no longer be committed. The panic happens before any commit work begins.
Source
Thrown at txn.go:611
req, err := txn.db.sendToWriteCh(entries)
if err != nil {
orc.doneCommit(commitTs)
return nil, err
}
ret := func() error {
err := req.Wait()
// Wait before marking commitTs as done.
// We can't defer doneCommit above, because it is being called from a
// callback here.
orc.doneCommit(commitTs)
return err
}
return ret, nil
}
func (txn *Txn) commitPrecheck() error {
if txn.discarded {
return errors.New("Trying to commit a discarded txn")
}
keepTogether := true
for _, e := range txn.pendingWrites {
if e.version != 0 {
keepTogether = false
}
}
// If keepTogether is True, it implies transaction markers will be added.
// In that case, commitTs should not be never be zero. This might happen if
// someone uses txn.Commit instead of txn.CommitAt in managed mode. This
// should happen only in managed mode. In normal mode, keepTogether will
// always be true.
if keepTogether && txn.db.opt.managedTxns && txn.commitTs == 0 {
return errors.New("CommitTs cannot be zero. Please use commitAt instead")
}
return nil
}View on GitHub (pinned to 2a001d466f)
Solutions
- Ensure Commit is called exactly once per Txn, before any Discard/Cancel
- Remove the early Discard/defer Discard that runs before Commit, or reorder so Discard happens only on the error path
- Do not retry Commit on a discarded Txn — create a new transaction with db.NewTransactionAt/db.NewTransaction instead
- Track commit state (e.g. a bool or sync.Once) if the Txn is shared across goroutines
Example fix
// before
txn := db.NewTransaction(true)
defer txn.Discard()
...
txn.Commit()
// after
txn := db.NewTransaction(true)
err := txn.Commit() // Commit marks txn discarded internally; no separate Discard needed
if err != nil {
txn.Discard()
return err
} Defensive patterns
Strategy: validation
Validate before calling
func canCommit(txn *badger.Txn) bool { return txn != nil } // track discarded via your own wrapper, since Txn.discarded is unexported
// Wrap the txn:
type TxnOnce struct { t *badger.Txn; done atomic.Bool }
func (w *TxnOnce) Commit() error {
if w.done.Swap(true) { return errors.New("txn already committed/discarded") }
return w.t.Commit()
} Type guard
func isUsable(w *TxnOnce) bool { return !w.done.Load() } Prevention
- Use defer txn.Discard() only on paths that do not Commit
- Never call Commit twice on one Txn; create a new txn for retries
- Wrap txns in a once-style helper if ownership is shared
When it happens
Trigger: Calling Discard() (explicitly or via defer) and later calling Commit() on the same Txn; calling Cancel() then Commit(); reusing a Txn after a prior Commit, since Commit also calls Discard internally.
Common situations: Double-defer patterns where one defer discards and later code commits; storing a Txn in a struct and committing after a request-scoped discard; committing after txn.Commit already ran (commit implies discard); error paths that Cancel then retry Commit on the same txn.
Related errors
AI-assisted analysis of dgraph-io/badger@2a001d466f (2026-09-05).
Data as JSON: /api/errors/88acee9f47596fa5.
Report an issue: GitHub.