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

  1. Make destroy() idempotent: return early when WatcherManager.instance is already null
  2. Snapshot before iterating: iterate over Array.from(this.projectWatchers)
  3. Audit the quit lifecycle so exactly one path calls destroy()
  4. 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

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


AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20). Data as JSON: /api/errors/7e95faadd24fc7bc. Report an issue: GitHub.