{"record":{"id":"7e95faadd24fc7bc","repo":"gitbutlerapp/gitbutler","slug":"failed-to-stop-project-watchers-during-shutdown","errorCode":null,"errorMessage":"Failed to stop project watchers during shutdown","messagePattern":"Failed to stop project watchers during shutdown","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"apps/lite/electron/src/watcher.ts","lineNumber":291,"sourceCode":"\t\t\tthis.senderSubscriptions.set(senderId, new Set([subscriptionId]));\n\t\t\treturn;\n\t\t}\n\n\t\tsubscriptions.add(subscriptionId);\n\t}\n\n\t/**\n\t * Stop all watchers and destroy the instance of the watcher manager.\n\t *\n\t * This needs to be called on application shotdown.\n\t */\n\tdestroy(): void {\n\t\ttry {\n\t\t\tthis.stopAllWatchersForShutdown();\n\t\t\tWatcherManager.instance = null;\n\t\t} catch (error) {\n\t\t\t// oxlint-disable-next-line no-console\n\t\t\tconsole.warn(\"Failed to stop project watchers during shutdown\", error);\n\t\t}\n\t}\n}\n","sourceCodeStart":273,"sourceCodeEnd":295,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/apps/lite/electron/src/watcher.ts#L273-L295","documentation":"destroy() is the shutdown entry point: it runs stopAllWatchersForShutdown() (whose individual stops are already caught) and then nulls the WatcherManager singleton. The outer catch only fires when bookkeeping itself throws — for example concurrent mutation of the maps during iteration, or a re-entrant destroy() — not for ordinary stop failures.","triggerScenarios":"Calling destroy() at apps/lite/electron/src/watcher.ts:291 while another control flow mutates projectWatchers or watcherSubscriptions (a concurrent unsubscribe during the iteration), or invoking destroy() twice so state is torn down mid-clear.","commonSituations":"Quit handlers wired twice ('before-quit' plus window 'closed' both calling destroy); races between unsubscribe storms and shutdown.","solutions":["Make destroy() idempotent: return early when WatcherManager.instance is already null","Snapshot before iterating: iterate over Array.from(this.projectWatchers)","Audit the quit lifecycle so exactly one path calls destroy()","Treat single occurrences at shutdown as noise; investigate only if reproducible mid-session"],"exampleFix":"// before\ndestroy(): void {\n\ttry {\n\t\tthis.stopAllWatchersForShutdown();\n\t\tWatcherManager.instance = null;\n\t} catch (error) {\n\t\t// oxlint-disable-next-line no-console\n\t\tconsole.warn(\"Failed to stop project watchers during shutdown\", error);\n\t}\n}\n\n// after\ndestroy(): void {\n\tif (!WatcherManager.instance) return; // idempotent shutdown\n\ttry {\n\t\tthis.stopAllWatchersForShutdown();\n\t} catch (error) {\n\t\t// oxlint-disable-next-line no-console\n\t\tconsole.warn(\"Failed to stop project watchers during shutdown\", error);\n\t}\n\tWatcherManager.instance = null;\n}","handlingStrategy":"validation","validationCode":"destroy(): void {\n\tif (WatcherManager.instance === null) return; // already destroyed\n\t/* ... */\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Wire destroy() into exactly one lifecycle hook","Guard singleton teardown against re-entry","Iterate over a snapshot of the maps during shutdown"],"tags":["typescript","electron","shutdown","singleton","cleanup"],"backgroundTag":"watcher-cleanup-failed","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}