Desktop crashes when second instance opens after window closed
Cluster 50491 · apps/desktop. Data as of
Nothing open on GitHub. Fix merged in #64800 on 16 Jul 2026. Check it covers your case.
The short answer
- Known?
- Yes. 3 related items grouped here, 1 issue already closed.
- Fix event
- A fix was merged to main in PR #64800. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
resolved, deciding PR #64800. That is separate from the fix event. - Still open
- Nothing open on GitHub.
A bug where Hermes Desktop crashes with 'Object has been destroyed' when a second instance is launched after the main window has been closed. The members cover the bug report and related fixes guarding or restoring the main window in the second-instance handler.
Open issues0
Open PRs0
Fixes on record
-
#64800 fix(desktop): restore closed main window on second launch
files:
apps/desktop/electron/main-window-lifecycle.test.ts,apps/desktop/electron/main-window-lifecycle.ts,apps/desktop/electron/main.tsShow the diff
-
apps/desktop/electron/main-window-lifecycle.test.ts@@ -0,0 +1,69 @@ +import assert from 'node:assert/strict' +import { test } from 'vitest' + +import { ensureMainWindow } from './main-window-lifecycle' + +test('recreates a destroyed primary window without focusing it', () => { + const destroyedWindow = { + isDestroyed: () => true + } + let createCalls = 0 + let focusCalls = 0 + + ensureMainWindow(destroyedWindow, { + isReady: true, + createWindow: () => { + createCalls += 1 + }, + focusWindow: () => { + focusCalls += 1 + } + }) + + assert.equal(createCalls, 1) + assert.equal(focusCalls, 0) +}) + +test('waits for app readiness before recreating a primary window', () => { + let createCalls = 0 + + ensureMainWindow(null, { + isReady: false, + createWindow: () => { + createCalls += 1 + }, + focusWindow: () => assert.fail('missing window must not be focused') + }) + + assert.equal(createCalls, 0) +}) + +test('focuses a live primary window for a normal second launch', () => { + const liveWindow = { + isDestroyed: () => false + } + let focusedWindow = null + + ensureMainWindow(liveWindow, { + isReady: true, + createWindow: () => assert.fail('live window must not be replaced'), + focusWindow: window => { + focusedWindow = window + } + }) + + assert.equal(focusedWindow, liveWindow) +}) + +test('leaves live-window focus to deep-link delivery', () => { + const liveWindow = { + isDestroyed: () => false + } + + ensureMainWindow(liveWindow, { + isReady: true, + createWindow: () => assert.fail('live window must not be replaced'), + focusWindow: () => assert.fail('deep-link delivery owns focus'), + focusExisting: false + }) +}) -
apps/desktop/electron/main-window-lifecycle.ts@@ -0,0 +1,28 @@ +type MainWindowLike = { + isDestroyed: () => boolean +} + +type EnsureMainWindowOptions<T extends MainWindowLike> = { + isReady: boolean + createWindow: () => unknown + focusWindow: (window: T) => unknown + focusExisting?: boolean +} + +export function ensureMainWindow<T extends MainWindowLike>( + window: T | null | undefined, + { isReady, createWindow, focusWindow, focusExisting = true }: EnsureMainWindowOptions<T> +) { + if (!window || window.isDestroyed()) { + // a closed electron window stays truthy, so replace it before invoking native methods. + if (isReady) { + createWindow() + } + + return + } + + if (focusExisting) { + focusWindow(window) + } +} -
apps/desktop/electron/main.ts@@ -95,6 +95,7 @@ import { TEXT_PREVIEW_SOURCE_MAX_BYTES } from './hardening' import { createLinkTitleWindow, guardLinkTitleSession, readLinkTitleWindowTitle } from './link-title-window' +import { ensureMainWindow } from './main-window-lifecycle' import { serializeJsonBody, setJsonRequestHeaders } from './oauth-net-request' import { decideProfileDeleteAction, profileNameFromDeleteRequest, resolveRouteProfile } from './profile-delete-routing' import { @@ -7234,10 +7235,17 @@ function createWindow() { mainWindow.on('unmaximize', schedulePersistWindowState) mainWindow.on('close', () => schedulePersistWindowState.flush()) - // The overlay rides the main window — closing the app's primary window must - // tear it down too (otherwise it strands as an orphan that blocks - // window-all-closed from quitting on Windows/Linux). - mainWindow.on('closed', () => closePetOverlay()) + // the closed wrapper remains truthy, so clear only the window this callback owns. + const createdMainWindow = mainWindow + mainWindow.on('closed', () => { + closePetOverlay() + + if (mainWindow === createdMainWindow) { + mainWindow = null + // the replacement renderer must register before queued links can be delivered. + _rendererReadyForDeepLink = false + } + }) wireCommonWindowHandlers(mainWindow, zoomWiringForWindowKind('chat')) @@ -8995,13 +9003,15 @@ if (!_gotSingleInstanceLock) { if (url) { handleDeepLink(url) - } else if (mainWindow) { - if (mainWindow.isMinimized()) { - mainWindow.restore() - } - - mainWindow.focus() } + + ensureMainWindow(mainWindow, { + isReady: app.isReady(), + createWindow, + focusWindow, + // deep-link delivery focuses a live window after its renderer is ready. + focusExisting: !url + }) }) }
-
1 PR closed without merging
From the close comments: other 1
- #50515 fix(desktop): guard second-instance focus against destroyed mainWindow (#50491) · other
Closed issues · 1
- Closed#50491[Bug]: Hermes Desktop crashes with "Object has been destroyed" when second-instance fires after mainWindow is closed
How this cluster was grouped
-
Link evidence (
structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference. -
Pairwise link density
0.67: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the LabAt a glance
- Subsystem
- apps/desktop
- Merged PRs
- #64800
- Closed issues
- 1
- Attempts
- 1 PR closed without merging
- Fix link checked
- 16 Jul 2026