gastownhall/beads · error

errClosed

errClosed

Error message

embeddeddolt: store is closed

What it means

errClosed is returned by any store method invoked after Close(): the connection helpers (withConn, withDBConn, withPinnedDBConn) and ApplySchemaMigrations check the closed flag first. IsClosed() exposes the same state so callers like maybeAutoCommit can skip work on a closed store without erroring.

Source

Thrown at internal/storage/embeddeddolt/store.go:91

const (
	// openStrict is the default: any pending-migration refusal fails the
	// open. Used by Open.
	openStrict openIntent = iota
	// openReadOnlyCommand relaxes both refusals for read-only commands: they
	// must keep working on the current schema until the operator makes the
	// migrate-or-adopt decision (bd-578h9.5), and must not be bricked by
	// dirty tables either. Used by OpenForReadOnlyCommand.
	openReadOnlyCommand
	// openWorkingSetReconcile relaxes both refusals for working-set-reconcile
	// commands (bd dolt commit, bd vc commit): their entire purpose is to
	// clear the dirty working set that a migration would otherwise refuse to
	// touch, so failing the open here would deadlock the documented recovery
	// (#4566). Used by OpenForWorkingSetReconcile.
	openWorkingSetReconcile
)

// errClosed is returned when a method is called after Close.
var errClosed = errors.New("embeddeddolt: store is closed")

// IsClosed reports whether the store has been closed. Implements
// storage.LifecycleManager so that callers (e.g., maybeAutoCommit) can
// skip operations on a closed store without triggering errClosed.
func (s *EmbeddedDoltStore) IsClosed() bool {
	return s.closed.Load()
}

// newStore creates an EmbeddedDoltStore using the embedded Dolt engine.
// beadsDir is the .beads/ root; the data directory is derived as <beadsDir>/embeddeddolt/.
// The database is created automatically if it doesn't exist (initSchema handles this).
//
// The dolthub/driver/v2 handles its own concurrency internally. File-level locking
// is only used during bd init (via util.TryLock in the init command) to protect
// one-time initialization steps — the store itself does not hold any lock.
func newStore(ctx context.Context, beadsDir, database, branch string, intent openIntent) (*EmbeddedDoltStore, error) {
	if database == "" {
		return nil, fmt.Errorf("embeddeddolt: database name must not be empty (caller should default to %q)", "beads")

View on GitHub (pinned to 71377f2769)

Solutions

  1. Check store.IsClosed() (or errors.Is(err, errClosed) via the exported detection) before operations in background/deferred paths
  2. Restructure so the store outlives all users — close only after goroutines have joined
  3. Use sync.WaitGroup/context cancellation so pending operations complete or abort before Close

Example fix

// before
store.Close()
go store.Flush(ctx) // returns errClosed
// after
err := store.Flush(ctx)
store.Close()
// or guard:
if !store.IsClosed() { _ = store.Flush(ctx) }
Defensive patterns

Strategy: type-guard

Validate before calling

if store.IsClosed() {
    return nil // skip background flush/commit
}

Type guard

func isOpen(store *embeddeddolt.EmbeddedDoltStore) bool { return !store.IsClosed() }

Try / catch

if err := store op(ctx); err != nil {
    if strings.Contains(err.Error(), "store is closed") { // prefer IsClosed() pre-check
        return nil
    }
    return err
}

Prevention

When it happens

Trigger: Calling any store operation after EmbeddedDoltStore.Close() has returned — e.g. background goroutines, deferred writes, or auto-commit paths racing with shutdown.

Common situations: Deferred/async work (flush, auto-commit, tip-metadata write) firing after a command finished and closed the store; using a store handle beyond its lifecycle scope.

Related errors


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