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

  1. Ensure Commit is called exactly once per Txn, before any Discard/Cancel
  2. Remove the early Discard/defer Discard that runs before Commit, or reorder so Discard happens only on the error path
  3. Do not retry Commit on a discarded Txn — create a new transaction with db.NewTransactionAt/db.NewTransaction instead
  4. 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

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.