dgraph-io/badger · error

This transaction has been discarded. Create a new one

Error message

This transaction has been discarded. Create a new one

What it means

ErrDiscardedTxn is panicked when operating on a Txn after Discard() (explicit, or via defer after Commit). NewIterator refuses to build a snapshot from a discarded txn, and since iterators like Rewind/Seek/Next call setIdx against the txn state, using an iterator after its txn was discarded surfaces this panic. Each transaction is single-use; you must create a new one after discarding.

Source

Thrown at iterator.go:461

	// iterators created by the stream interface
	ThreadId int

	Alloc *z.Allocator
}

// NewIterator returns a new iterator. Depending upon the options, either only keys, or both
// key-value pairs would be fetched. The keys are returned in lexicographically sorted order.
// Using prefetch is recommended if you're doing a long running iteration, for performance.
//
// Multiple Iterators:
// For a read-only txn, multiple iterators can be running simultaneously. However, for a read-write
// txn, iterators have the nuance of being a snapshot of the writes for the transaction at the time
// iterator was created. If writes are performed after an iterator is created, then that iterator
// will not be able to see those writes. Only writes performed before an iterator was created can be
// viewed.
func (txn *Txn) NewIterator(opt IteratorOptions) *Iterator {
	if txn.discarded {
		panic(ErrDiscardedTxn)
	}
	if txn.db.IsClosed() {
		panic(ErrDBClosed)
	}

	y.NumIteratorsCreatedAdd(txn.db.opt.MetricsEnabled, 1)

	// Keep track of the number of active iterators.
	txn.numIterators.Add(1)

	// TODO: If Prefix is set, only pick those memtables which have keys with the prefix.
	tables, decr := txn.db.getMemTables()
	defer decr()
	txn.db.vlog.incrIteratorCount()
	var iters []y.Iterator
	if itr := txn.newPendingWritesIterator(opt.Reverse); itr != nil {
		iters = append(iters, itr)
	}

View on GitHub (pinned to 2a001d466f)

Solutions

  1. Create the iterator inside the same scope/lifetime as the transaction and finish iteration before Commit/Discard
  2. If the txn was discarded, call db.NewTransaction again and build a fresh iterator
  3. Fix goroutine ownership: pass the iterator and txn together and never use them after the owning function returns

Example fix

// before
txn := db.NewTransaction(false)
defer txn.Discard()
it := txn.NewIterator(opts)
go func(){ for it.Rewind(); it.Valid(); it.Next() {} }() // races discard
// after
txn := db.NewTransaction(false)
it := txn.NewIterator(opts)
for it.Rewind(); it.Valid(); it.Next() {}
it.Close()
txn.Discard()
Defensive patterns

Strategy: try-catch

Validate before calling

if txn == nil || txn.Discarded() { txn = db.NewTransaction(false) }

Try / catch

func safeIter(db *badger.DB, opts badger.IteratorOptions) {
    defer func(){ recover() }() // iterator APIs panic on discarded txn
    txn := db.NewTransaction(false)
    defer txn.Discard()
    it := txn.NewIterator(opts)
    defer it.Close()
    for it.Rewind(); it.Valid(); it.Next() {}
}

Prevention

When it happens

Trigger: Calling txn.NewIterator after txn.Discard() or txn.Commit(); holding an Iterator and calling Rewind/Seek/Next after the enclosing transaction was committed/discarded; reusing a txn returned by db.NewTransaction across an err return path that already discarded it.

Common situations: Long-lived iterators in loops that outlive the txn; defer txn.Discard() executed early via function return while a goroutine still iterates; err paths that discard then retry with the same txn object.

Related errors


AI-assisted analysis of dgraph-io/badger@2a001d466f (2026-09-05). Data as JSON: /api/errors/046dcb25b728b99b. Report an issue: GitHub.