Which ipcRenderer.invoke promise settles when the matched ipcMain.handle never replies?
0 reputation · 10 Oct 2021, 11:51 UTC
0 reputation · 10 Oct 2021, 11:51 UTC
The goal is to characterize the runtime behavior of Electron's ipcRenderer.invoke when the corresponding ipcMain.handle never transmits a reply. This scenario arises in long-running or faulted main-process handlers where no explicit resolution occurs. Understanding whether the promise remains pending, rejects after an implicit timeout, or causes renderer resource retention is essential for designing robust inter‑process communication.
A primary constraint is that Electron provides no built‑in timeout or cancellation for invoke; the Promise returned by ipcRenderer.resolve only settles when the handler calls reply(). Without a handler, the promise rejects immediately, but a silent non‑response leaves the promise in a pending state indefinitely. The renderer lacks API‑level mechanisms to distinguish a slow handler from a dropped connection, and pending promises may retain renderer objects, potentially blocking UI logic that awaits them.
35775 reputation · 10 Oct 2021, 19:05 UTC
When an ipcMain.handle never sends a reply, the ipcRenderer.invoke promise stays pending forever. Electron’s IPC layer does not impose a timeout or automatic rejection; the promise only settles when the main process calls event.reply() or returns a value from the handler.
Promise object remains in memory.Because the renderer cannot detect a silent main‑process handler, you must add a client‑side timeout wrapper:
function safeInvoke(channel, ...args) {
const timeoutMs = 5000; // adjust as needed
return Promise.race([
ipcRenderer.invoke(channel, ...args),
new Promise((_, reject) => setTimeout(() => reject(new Error('invoke timed out')), timeoutMs))
]);
}
Note that rejecting the wrapper does not cancel the main‑process handler; it may continue running and eventually send a reply that will be ignored by the renderer.
While the promise is pending, the renderer’s IPC layer holds a reference to the callback closure. Any objects captured by that closure—such as DOM nodes or large buffers—remain reachable, potentially delaying garbage collection of the entire renderer context until the page is navigated, reloaded, or the renderer process is killed.
main.js, register a handler that never replies:
ipcMain.handle('slow', () => {/* intentionally no reply */});
ipcRenderer.invoke('slow').then(console.log).catch(console.error);
safeInvoke above and confirm that it rejects after the timeout while the raw promise remains pending.The behavior can vary slightly across Electron major releases. If you notice a different outcome, please share the exact Electron version you’re using.
Use comments to ask for clarification. Post a solution as an answer.
35,775 reputation · 10 Oct 2021, 13:57 UTC
When ipcRenderer.invoke has no matching handler or the handler never resolves, the renderer's IPC manager maintains an internal map of pending callbacks keyed by message ID. Each entry holds a reference to the resolver function and its captured closure. This prevents garbage collection of any objects referenced in the handler's calling context—DOM elements, large buffers, or component instances.
Wrap invoke calls with a timeout using Promise.race, but understand that rejection does not clean up the main-process handler. For long-running operations, consider implementing a heartbeat or cancellation protocol where the main process periodically acknowledges progress.
ipcMain.handle that delays response beyond UI interaction lifetimeElectron versions prior to 28 lack built-in diagnostics for stalled IPC channels, making manual tracking essential for production stability.