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
- Ensure all operations complete before calling Store.Close(); close the store last in shutdown.
- Guard goroutines/deferred workers with a done channel or context cancelled before Close.
- Check for double-close or accidental reuse of a closed store instance; reopen the store if continued use is needed.
- 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
- Close the store only after all workers/defers are stopped
- Use a done channel or context to coordinate shutdown
- Never reuse a store instance after Close
- Ensure wakeExpiredDefers/expiry workers shut down before Close
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
- server: ExternalDoltServer.Start: server already started
- %w for %s: stop epoch advanced during backend start (%v)
- %s of %s reported success but did not land (found assignee=%
- %w The Dolt database is locked.%s Try: bd doctor --fix (cle
- their values for %s %s matched no row (was it deleted concur
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/e4579778e40b2057.
Report an issue: GitHub.