dgraph-io/badger · error

Unclosed iterator at time of Txn.Discard.

Error message

Unclosed iterator at time of Txn.Discard.

What it means

Txn.Discard() panics if the transaction still has open iterators (txn.numIterators > 0) at discard time. Iterators hold read state against the transaction; discarding without closing them first would leak oracle read-mark resources. The panic enforces the invariant that every it.NewIterator must be matched by it.Close() before Discard.

Source

Thrown at txn.go:513

		// the same time. The reads slice is not currently thread-safe and
		// needs to be locked whenever we mark a key as read.
		txn.readsLock.Lock()
		txn.reads = append(txn.reads, fp)
		txn.readsLock.Unlock()
	}
}

// Discard discards a created transaction. This method is very important and must be called. Commit
// method calls this internally, however, calling this multiple times doesn't cause any issues. So,
// this can safely be called via a defer right when transaction is created.
//
// NOTE: If any operations are run on a discarded transaction, ErrDiscardedTxn is returned.
func (txn *Txn) Discard() {
	if txn.discarded { // Avoid a re-run.
		return
	}
	if txn.numIterators.Load() > 0 {
		panic("Unclosed iterator at time of Txn.Discard.")
	}
	txn.discarded = true
	if !txn.db.orc.isManaged {
		txn.db.orc.doneRead(txn)
	}
}

func (txn *Txn) commitAndSend() (func() error, error) {
	orc := txn.db.orc
	// Ensure that the order in which we get the commit timestamp is the same as
	// the order in which we push these updates to the write channel. So, we
	// acquire a writeChLock before getting a commit timestamp, and only release
	// it after pushing the entries to it.
	orc.writeChLock.Lock()
	defer orc.writeChLock.Unlock()

	commitTs, conflict := orc.newCommitTs(txn)
	if conflict {

View on GitHub (pinned to 2a001d466f)

Solutions

  1. Always defer it.Close() immediately after txn.NewIterator(opts), before any early returns
  2. Ensure iterator lifetime is strictly nested inside the transaction's lifetime (close iterator before Commit/Discard)
  3. On error paths that abandon iteration, close the iterator explicitly before returning
  4. If using Stream/WriteBatch internals, do not manually Discard transactions that the stream framework owns

Example fix

// before
it := txn.NewIterator(opts)
for it.Rewind(); it.Valid(); it.Next() {
    if err := process(it.Item()); err != nil {
        return err // iterator never closed -> Discard panics
    }
}
// after
it := txn.NewIterator(opts)
defer it.Close()
for it.Rewind(); it.Valid(); it.Next() {
    if err := process(it.Item()); err != nil {
        return err // deferred Close runs before deferred Discard
    }
}
Defensive patterns

Strategy: try-catch

Try / catch

// ensure cleanup order via nested defers:
txn := db.NewTransaction(true)
defer txn.Discard()
it := txn.NewIterator(opts)
defer it.Close() // LIFO: Close runs before Discard
for it.Rewind(); it.Valid(); it.Next() { ... }

Prevention

When it happens

Trigger: Returning from a function where a defer txn.Discard() runs but an Iterator created from that txn was never Closed; panics in the middle of an iteration skipping the it.Close() defer; committing/discarding a txn while an iterator created with the PrefillValues/streaming options (yieldItemValue/produceKVs paths) is still open.

Common situations: Early return inside it.Seek/iter loops without closing the iterator; a panic/error path that unwinds past the iterator close; holding an iterator across a callback that commits the txn; stream.Send paths in Stream framework where iterators are managed internally and user code discards the txn early.

Related errors


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