Electron Ipc
IPC communication patterns for Electron main and renderer process messaging
You are an expert in Electron's Inter-Process Communication (IPC) system for building secure, well-structured communication between main and renderer processes.
## Key Points
1. **Renderer to Main** (one-way): `ipcRenderer.send` / `ipcMain.on`
2. **Renderer to Main with response** (two-way): `ipcRenderer.invoke` / `ipcMain.handle`
3. **Main to Renderer** (push): `webContents.send` / `ipcRenderer.on`
- Always use `ipcMain.handle` / `ipcRenderer.invoke` for request-response patterns. It returns a Promise and handles errors cleanly.
- Whitelist IPC channels in the preload script. Never expose a generic `ipcRenderer.send` without validation.
- Define channel names as shared constants to avoid typos and enable easy refactoring.
- Validate all data received in `ipcMain` handlers. Treat renderer input as untrusted.
- Always remove IPC listeners when they are no longer needed to prevent memory leaks.
- Use structured return types (`{ success, data, error }`) for consistent error handling.
- Keep IPC handlers thin — delegate to service modules for business logic.
- **Exposing `ipcRenderer` directly**: Never do `contextBridge.exposeInMainWorld('ipc', ipcRenderer)`. This gives the renderer full access to all IPC channels.
- **Forgetting to remove listeners**: `ipcRenderer.on` persists across page navigations in the same window. Always return and call cleanup functions.
## Quick Example
```javascript
// renderer.js
document.getElementById('btn-minimize').addEventListener('click', () => {
window.api.send('window:minimize');
});
```skilldb get desktop-app-skills/Electron IpcFull skill: 310 linesElectron IPC — Desktop Apps
You are an expert in Electron's Inter-Process Communication (IPC) system for building secure, well-structured communication between main and renderer processes.
Overview
Electron's IPC (Inter-Process Communication) is the mechanism by which the main process and renderer processes exchange messages. Since renderer processes run in a sandboxed Chromium environment and the main process has full Node.js access, IPC is the only safe way to bridge them. Modern Electron enforces this through contextIsolation and preload scripts that expose a controlled API surface via contextBridge.
Three primary IPC patterns:
- Renderer to Main (one-way):
ipcRenderer.send/ipcMain.on - Renderer to Main with response (two-way):
ipcRenderer.invoke/ipcMain.handle - Main to Renderer (push):
webContents.send/ipcRenderer.on
Setup & Configuration
Preload script foundation
// preload.js
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('api', {
// Two-way: renderer invokes, main responds
invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args),
// One-way: renderer fires, main handles
send: (channel, ...args) => ipcRenderer.send(channel, ...args),
// Listen for messages from main
on: (channel, callback) => {
const subscription = (_event, ...args) => callback(...args);
ipcRenderer.on(channel, subscription);
return () => ipcRenderer.removeListener(channel, subscription);
},
// Listen once
once: (channel, callback) => {
ipcRenderer.once(channel, (_event, ...args) => callback(...args));
},
});
Secure preload with channel whitelisting
// preload.js — production-grade with explicit channel allowlists
const { contextBridge, ipcRenderer } = require('electron');
const INVOKE_CHANNELS = [
'db:query',
'db:insert',
'file:read',
'file:write',
'config:get',
'config:set',
];
const SEND_CHANNELS = [
'window:minimize',
'window:close',
'app:log',
];
const RECEIVE_CHANNELS = [
'app:update-available',
'app:download-progress',
'navigation:go',
];
contextBridge.exposeInMainWorld('api', {
invoke: (channel, ...args) => {
if (!INVOKE_CHANNELS.includes(channel)) {
throw new Error(`IPC invoke not allowed on channel: ${channel}`);
}
return ipcRenderer.invoke(channel, ...args);
},
send: (channel, ...args) => {
if (!SEND_CHANNELS.includes(channel)) {
throw new Error(`IPC send not allowed on channel: ${channel}`);
}
ipcRenderer.send(channel, ...args);
},
on: (channel, callback) => {
if (!RECEIVE_CHANNELS.includes(channel)) {
throw new Error(`IPC listen not allowed on channel: ${channel}`);
}
const subscription = (_event, ...args) => callback(...args);
ipcRenderer.on(channel, subscription);
return () => ipcRenderer.removeListener(channel, subscription);
},
});
Core Patterns
Pattern 1: Request-Response with invoke / handle
This is the most common and recommended pattern. It returns a Promise.
// main.js
const { ipcMain } = require('electron');
const fs = require('node:fs/promises');
ipcMain.handle('file:read', async (_event, filePath) => {
try {
const content = await fs.readFile(filePath, 'utf-8');
return { success: true, data: content };
} catch (err) {
return { success: false, error: err.message };
}
});
ipcMain.handle('db:query', async (_event, sql, params) => {
// Validate inputs before executing
if (typeof sql !== 'string') throw new Error('SQL must be a string');
const results = await database.query(sql, params);
return results;
});
// renderer.js
async function loadFile(path) {
const result = await window.api.invoke('file:read', path);
if (result.success) {
document.getElementById('editor').value = result.data;
} else {
showError(result.error);
}
}
Pattern 2: One-way fire-and-forget with send / on
Use when the renderer doesn't need a response.
// main.js
const { ipcMain, BrowserWindow } = require('electron');
ipcMain.on('window:minimize', (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
win?.minimize();
});
ipcMain.on('window:close', (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
win?.close();
});
ipcMain.on('app:log', (_event, level, message) => {
logger[level]?.(message);
});
// renderer.js
document.getElementById('btn-minimize').addEventListener('click', () => {
window.api.send('window:minimize');
});
Pattern 3: Main pushes to renderer
// main.js — push updates to a specific window
function notifyRenderer(win, channel, ...args) {
if (win && !win.isDestroyed()) {
win.webContents.send(channel, ...args);
}
}
// Example: push download progress
function startDownload(win, url) {
downloadManager.on('progress', (percent) => {
notifyRenderer(win, 'app:download-progress', percent);
});
downloadManager.start(url);
}
// renderer.js — listen for pushes from main
const unsubscribe = window.api.on('app:download-progress', (percent) => {
progressBar.style.width = `${percent}%`;
progressLabel.textContent = `${percent}%`;
});
// Clean up when done
// unsubscribe();
Pattern 4: Typed IPC with a service layer
// shared/ipc-channels.js — single source of truth
const IPC = {
FILE_READ: 'file:read',
FILE_WRITE: 'file:write',
FILE_PICK: 'file:pick',
CONFIG_GET: 'config:get',
CONFIG_SET: 'config:set',
WINDOW_MINIMIZE: 'window:minimize',
WINDOW_CLOSE: 'window:close',
};
module.exports = { IPC };
// main.js — register handlers using shared constants
const { IPC } = require('./shared/ipc-channels');
ipcMain.handle(IPC.FILE_READ, handleFileRead);
ipcMain.handle(IPC.FILE_WRITE, handleFileWrite);
ipcMain.handle(IPC.CONFIG_GET, handleConfigGet);
ipcMain.handle(IPC.CONFIG_SET, handleConfigSet);
Pattern 5: Window-to-window communication via main relay
// main.js — relay messages between windows
ipcMain.on('relay', (event, targetWindowId, channel, ...args) => {
const targetWin = BrowserWindow.fromId(targetWindowId);
if (targetWin && !targetWin.isDestroyed()) {
targetWin.webContents.send(channel, ...args);
}
});
Pattern 6: Bidirectional streaming with MessagePort
// main.js
const { MessageChannelMain } = require('electron');
ipcMain.handle('create-worker-channel', (event) => {
const { port1, port2 } = new MessageChannelMain();
// Send port2 to the renderer
event.sender.postMessage('port', null, [port2]);
// Use port1 in main process
port1.on('message', (event) => {
console.log('Received from renderer:', event.data);
port1.postMessage({ response: 'acknowledged' });
});
port1.start();
});
// preload.js — expose port receiver
const { ipcRenderer } = require('electron');
ipcRenderer.on('port', (event) => {
const port = event.ports[0];
contextBridge.exposeInMainWorld('workerPort', {
send: (data) => port.postMessage(data),
onMessage: (cb) => { port.onmessage = (e) => cb(e.data); },
});
});
Best Practices
- Always use
ipcMain.handle/ipcRenderer.invokefor request-response patterns. It returns a Promise and handles errors cleanly. - Whitelist IPC channels in the preload script. Never expose a generic
ipcRenderer.sendwithout validation. - Define channel names as shared constants to avoid typos and enable easy refactoring.
- Validate all data received in
ipcMainhandlers. Treat renderer input as untrusted. - Always remove IPC listeners when they are no longer needed to prevent memory leaks.
- Use structured return types (
{ success, data, error }) for consistent error handling. - Keep IPC handlers thin — delegate to service modules for business logic.
Common Pitfalls
- Exposing
ipcRendererdirectly: Never docontextBridge.exposeInMainWorld('ipc', ipcRenderer). This gives the renderer full access to all IPC channels. - Forgetting to remove listeners:
ipcRenderer.onpersists across page navigations in the same window. Always return and call cleanup functions. - Using
ipcMain.onwhenhandleis better:ondoesn't support returning values cleanly. Usehandle/invokefor request-response. - Sending non-serializable data: IPC uses the structured clone algorithm. Functions, DOM nodes, and circular references cannot be sent.
- Not checking
event.sender: In multi-window apps, verify which window sent the message before acting on it. - Large payloads: Sending huge buffers (100+ MB) over IPC can freeze the app. Stream large data through files or
MessagePortinstead.
Core Philosophy
IPC is the security boundary of your Electron application. The renderer process should be treated as an untrusted client — just as you would treat a browser making requests to your API server. Every message received in the main process must be validated, typed, and handled defensively. The preload script is the controlled gateway that determines exactly what the renderer can and cannot do.
Prefer invoke/handle over send/on for any interaction where the caller needs a response. The invoke pattern returns a Promise, handles errors cleanly, and establishes a clear request-response contract. Fire-and-forget (send/on) is appropriate for notifications and logging, but using it for data operations leads to lost responses and unhandled errors.
Define your IPC channels as a shared contract, not as magic strings scattered across files. A single constants file listing all valid channel names prevents typos, enables IDE autocompletion, and makes it trivial to audit the full API surface exposed to the renderer. If a channel is not in the allowlist, it should not exist.
Anti-Patterns
-
Exposing raw
ipcRendererto the renderer — callingcontextBridge.exposeInMainWorld('ipc', ipcRenderer)gives the renderer unrestricted access to every IPC channel, eliminating the security benefit of context isolation. -
Using magic string channel names — scattering channel names like
'file:read'as bare strings across the codebase leads to typos and makes the IPC API surface impossible to audit; define channels as shared constants. -
Not validating IPC handler inputs — accepting renderer-supplied file paths, SQL queries, or configuration values without type checking and sanitization exposes the main process to injection attacks.
-
Forgetting to remove listeners —
ipcRenderer.onpersists across SPA navigations within the same window; failing to clean up causes memory leaks and duplicate handler execution. -
Sending non-serializable data — IPC uses the structured clone algorithm; functions, DOM nodes, and circular references cannot be transmitted and will throw or silently fail.
Install this skill directly: skilldb add desktop-app-skills
Related Skills
Auto Update
Auto-update mechanisms for desktop applications using electron-updater and Tauri's built-in updater
Electron
Electron fundamentals for building cross-platform desktop applications with web technologies
File System Access
File system access patterns, native dialogs, and drag-and-drop for desktop applications
Native Menus
System tray icons, native application menus, and desktop notifications for Electron and Tauri apps
Packaging
Building, packaging, and distributing desktop applications for Windows, macOS, and Linux
Security
Desktop application security including context isolation, preload scripts, CSP, and sandboxing for Electron and Tauri