gitbutlerapp/gitbutler · warning
Failed to stop project watchers during shutdown
Error message
Failed to stop project watchers during shutdown
What it means
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.
Source
Thrown at apps/lite/electron/src/watcher.ts:291
this.senderSubscriptions.set(senderId, new Set([subscriptionId]));
return;
}
subscriptions.add(subscriptionId);
}
/**
* Stop all watchers and destroy the instance of the watcher manager.
*
* This needs to be called on application shotdown.
*/
destroy(): void {
try {
this.stopAllWatchersForShutdown();
WatcherManager.instance = null;
} catch (error) {
// oxlint-disable-next-line no-console
console.warn("Failed to stop project watchers during shutdown", error);
}
}
}
View on GitHub (pinned to caf1f223d3)
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
Example fix
// before
destroy(): void {
try {
this.stopAllWatchersForShutdown();
WatcherManager.instance = null;
} catch (error) {
// oxlint-disable-next-line no-console
console.warn("Failed to stop project watchers during shutdown", error);
}
}
// after
destroy(): void {
if (!WatcherManager.instance) return; // idempotent shutdown
try {
this.stopAllWatchersForShutdown();
} catch (error) {
// oxlint-disable-next-line no-console
console.warn("Failed to stop project watchers during shutdown", error);
}
WatcherManager.instance = null;
} Defensive patterns
Strategy: validation
Validate before calling
destroy(): void {
if (WatcherManager.instance === null) return; // already destroyed
/* ... */
} Prevention
- Wire destroy() into exactly one lifecycle hook
- Guard singleton teardown against re-entry
- Iterate over a snapshot of the maps during shutdown
When it happens
Trigger: 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.
Common situations: Quit handlers wired twice ('before-quit' plus window 'closed' both calling destroy); races between unsubscribe storms and shutdown.
Related errors
- Failed to stop project watcher
- Failed to stop project watcher for ${projectId}
- Watcher callback failed
- unable to log error to file
- SilentError suppressed from query:error telemetry
AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20).
Data as JSON: /api/errors/7e95faadd24fc7bc.
Report an issue: GitHub.