gastownhall/beads · error

ErrStoreClosed

ErrStoreClosed

Error message

store is closed

What it means

ErrStoreClosed is returned when an operation is attempted on a Dolt store that has already been closed. It is a lifecycle sentinel checked by operations such as IterIssues, iterIssuesWithDepType, and wakeExpiredDefers before touching the underlying connection. It protects against use-after-close of the store's SQL connection pool.

Source

Thrown at internal/storage/dolt/store.go:1006

		return q[:300] + "…"
	}
	return q
}

// endSpan records an error (if any) and ends the span.
func endSpan(span trace.Span, err error) {
	if err != nil {
		span.RecordError(err)
		span.SetStatus(codes.Error, err.Error())
	}
	span.End()
}

// execContext wraps a write statement in an explicit BEGIN/COMMIT to ensure
// durability when the Dolt server runs with autocommit disabled (the default
// when started with --no-auto-commit). Without this, writes remain in an
// ErrStoreClosed is returned when an operation is attempted on a closed store.
var ErrStoreClosed = errors.New("store is closed")

// withReadTx runs fn inside a transaction while holding the store's read-lock.
// Used for read operations that need a *sql.Tx to share issueops functions.
//
// The whole BeginTx+fn is wrapped in withRetry so a transient connection error
// (e.g. "invalid connection" when the dolt sql-server reaps a pooled connection
// that has been idle past its wait_timeout) is retried rather than surfaced to
// the caller. This is safe because fn is read-only and the transaction is always
// rolled back, so re-running the operation has no side effects.
func (s *DoltStore) withReadTx(ctx context.Context, fn func(tx *sql.Tx) error) error {
	if s.closed.Load() {
		return ErrStoreClosed
	}
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.withRetry(ctx, func() error {
		tx, err := s.db.BeginTx(ctx, nil)
		if err != nil {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Ensure all operations complete before calling Store.Close(); close the store last in shutdown.
  2. Guard goroutines/deferred workers with a done channel or context cancelled before Close.
  3. Check for double-close or accidental reuse of a closed store instance; reopen the store if continued use is needed.
  4. If it appears in wakeExpiredDefers, ensure the expiry worker is stopped before closing the store.

Example fix

// before
store.Close()
issues, err := store.IterIssues(ctx) // ErrStoreClosed
// after
issues, err := store.IterIssues(ctx)
if err == nil {
    store.Close()
}
Defensive patterns

Strategy: type-guard

Validate before calling

// guard before use
select {
case <-storeClosed:
    return ErrStoreClosed
default:
}

Type guard

func isStoreClosed(err error) bool { return errors.Is(err, dolt.ErrStoreClosed) }

Try / catch

if errors.Is(err, dolt.ErrStoreClosed) {
    // reopen the store or terminate the worker gracefully
}

Prevention

When it happens

Trigger: Calling any store read/write method (e.g. IterIssues) after Store.Close() has been called, or concurrently while Close is in progress — e.g. a background goroutine or deferred task running after shutdown.

Common situations: App shutdown ordering issues: background defers/expiry workers still holding the store after Close; double-Close then reuse; long-running query goroutines outliving the CLI command's store lifetime.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/e4579778e40b2057. Report an issue: GitHub.