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
- Create the iterator inside the same scope/lifetime as the transaction and finish iteration before Commit/Discard
- If the txn was discarded, call db.NewTransaction again and build a fresh iterator
- 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
- Scope iterators to the transaction's lifetime; close iterator before Discard/Commit
- Never share txn or Iterator across goroutines
- Recreate the transaction after any Discard
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
- Unclosed iterator at time of Txn.Discard.
- DB Closed
- opt.Prefix should be nil for NewKeyIterator.
- ErrDiscardedTxn
- ErrRejected
AI-assisted analysis of dgraph-io/badger@2a001d466f (2026-09-05).
Data as JSON: /api/errors/046dcb25b728b99b.
Report an issue: GitHub.