{"record":{"id":"ca16acfc846283e7","repo":"gastownhall/beads","slug":"errclosed","errorCode":"errClosed","errorMessage":"embeddeddolt: store is closed","messagePattern":"embeddeddolt: store is closed","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/embeddeddolt/store.go","lineNumber":91,"sourceCode":"const (\n\t// openStrict is the default: any pending-migration refusal fails the\n\t// open. Used by Open.\n\topenStrict openIntent = iota\n\t// openReadOnlyCommand relaxes both refusals for read-only commands: they\n\t// must keep working on the current schema until the operator makes the\n\t// migrate-or-adopt decision (bd-578h9.5), and must not be bricked by\n\t// dirty tables either. Used by OpenForReadOnlyCommand.\n\topenReadOnlyCommand\n\t// openWorkingSetReconcile relaxes both refusals for working-set-reconcile\n\t// commands (bd dolt commit, bd vc commit): their entire purpose is to\n\t// clear the dirty working set that a migration would otherwise refuse to\n\t// touch, so failing the open here would deadlock the documented recovery\n\t// (#4566). Used by OpenForWorkingSetReconcile.\n\topenWorkingSetReconcile\n)\n\n// errClosed is returned when a method is called after Close.\nvar errClosed = errors.New(\"embeddeddolt: store is closed\")\n\n// IsClosed reports whether the store has been closed. Implements\n// storage.LifecycleManager so that callers (e.g., maybeAutoCommit) can\n// skip operations on a closed store without triggering errClosed.\nfunc (s *EmbeddedDoltStore) IsClosed() bool {\n\treturn s.closed.Load()\n}\n\n// newStore creates an EmbeddedDoltStore using the embedded Dolt engine.\n// beadsDir is the .beads/ root; the data directory is derived as <beadsDir>/embeddeddolt/.\n// The database is created automatically if it doesn't exist (initSchema handles this).\n//\n// The dolthub/driver/v2 handles its own concurrency internally. File-level locking\n// is only used during bd init (via util.TryLock in the init command) to protect\n// one-time initialization steps — the store itself does not hold any lock.\nfunc newStore(ctx context.Context, beadsDir, database, branch string, intent openIntent) (*EmbeddedDoltStore, error) {\n\tif database == \"\" {\n\t\treturn nil, fmt.Errorf(\"embeddeddolt: database name must not be empty (caller should default to %q)\", \"beads\")","sourceCodeStart":73,"sourceCodeEnd":109,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/embeddeddolt/store.go#L73-L109","documentation":"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.","triggerScenarios":"Calling any store operation after EmbeddedDoltStore.Close() has returned — e.g. background goroutines, deferred writes, or auto-commit paths racing with shutdown.","commonSituations":"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.","solutions":["Check store.IsClosed() (or errors.Is(err, errClosed) via the exported detection) before operations in background/deferred paths","Restructure so the store outlives all users — close only after goroutines have joined","Use sync.WaitGroup/context cancellation so pending operations complete or abort before Close"],"exampleFix":"// before\nstore.Close()\ngo store.Flush(ctx) // returns errClosed\n// after\nerr := store.Flush(ctx)\nstore.Close()\n// or guard:\nif !store.IsClosed() { _ = store.Flush(ctx) }","handlingStrategy":"type-guard","validationCode":"if store.IsClosed() {\n    return nil // skip background flush/commit\n}","typeGuard":"func isOpen(store *embeddeddolt.EmbeddedDoltStore) bool { return !store.IsClosed() }","tryCatchPattern":"if err := store op(ctx); err != nil {\n    if strings.Contains(err.Error(), \"store is closed\") { // prefer IsClosed() pre-check\n        return nil\n    }\n    return err\n}","preventionTips":["Check IsClosed() in all background/deferred paths before touching the store","Close the store only after all goroutines have completed (WaitGroup)","Keep store lifetime scoped to the command; never cache handles past Close"],"tags":["go","embedded-dolt","lifecycle","use-after-close"],"backgroundTag":"use-after-close","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}