Die IPC-Kommunikation in Electron läuft über zwei Module: ipcMain im Main-Prozess als Server-Seite und ipcRenderer im Renderer als Client-Seite. Hier die APIs beider Module im Vergleich, mit den zwei Patterns send/on und invoke/handle.

Die zwei Module

ModulWoAufgabe
ipcMainMain-ProzessEmpfängt Nachrichten von Renderern, kann an Renderer pushen
ipcRendererRenderer / PreloadSendet Nachrichten an Main, empfängt vom Main

Sie sprechen über benannte Channels (frei wählbare Strings), und zwar in zwei Varianten: das klassische send/on (Fire-and-Forget mit Events) und das modernere invoke/handle (Promise-basiert mit Rückgabe).

ipcMain — die Server-Seite

JavaScript main.js
import { app, BrowserWindow, ipcMain } from 'electron';

// Pattern 1: invoke/handle — modern, Promise-basiert
ipcMain.handle('app:get-version', () => app.getVersion());
ipcMain.handle('files:read', async (_event, path) => {
    return fs.readFile(path, 'utf-8');
});

// Pattern 2: send/on — klassisch, Fire-and-Forget
ipcMain.on('analytics:event', (_event, eventName, payload) => {
    sendToAnalytics(eventName, payload);
});

ipcMain.handle(channel, handler) registriert einen Promise-fähigen Endpunkt. ipcMain.on(channel, handler) einen klassischen Event-Listener ohne Rückgabewert.

Pro Channel und Variante darfst du nur einen Handler haben — handle darf nicht doppelt registriert sein.

ipcRenderer — die Client-Seite

JavaScript preload.js
import { contextBridge, ipcRenderer } from 'electron';

contextBridge.exposeInMainWorld('api', {
    // Pattern 1: invoke — Promise zurück
    getVersion: () => ipcRenderer.invoke('app:get-version'),
    readFile:   (path) => ipcRenderer.invoke('files:read', path),

    // Pattern 2: send — kein Rückgabewert
    track: (eventName, payload) => ipcRenderer.send('analytics:event', eventName, payload)
});

Im Renderer-Code dann:

JavaScript renderer.js
const v = await window.api.getVersion();
const text = await window.api.readFile('/path/to/file');
window.api.track('button_click', { id: 'submit' });

Das Event-Objekt

Der Handler im Main bekommt als erstes Argument ein event-Objekt — mit Metadaten zum Sender:

JavaScript
ipcMain.handle('files:read', async (event, path) => {
    console.log('Anfrage von Frame-ID:', event.frameId);
    console.log('Anfrage von WebContents:', event.sender.id);

    // Antwort gezielt an dieses Window pushen
    event.sender.send('progress:update', { percent: 50 });

    return fs.readFile(path, 'utf-8');
});

event.sender ist die webContents-Instanz des sendenden Renderers — wichtig für „Antwort nur an diesen einen Renderer schicken", besonders bei Multi-Window-Apps.

Vergleich der zwei Patterns

invoke / handlesend / on
RichtungRenderer → Main mit AntwortBeide Richtungen, ohne Antwort
Return-ValueJa, als PromiseNein
Error-HandlingPromise rejected automatischManuell
Use-CaseDaten holen, RPC-artige CallsFire-and-Forget, Push-Events
Empfohlen seitElectron 7Klassisch

Faustregel: invoke/handle für jeden Renderer-zu-Main-Call mit Rückgabe. send/on nur für reine Push-Events vom Main an den Renderer (webContents.send + ipcRenderer.on) oder Fire-and-Forget-Logging.

Channel-Naming

Channels sind nur Strings — Typisierung gibt es nicht eingebaut. Konventionen helfen:

JavaScript
// gut: namespace:action
'app:get-version'
'files:read'
'files:write'
'theme:set'
'window:minimize'

// weniger gut: flach
'getVersion'
'readFile'
'minimize'

Mit Namespaces siehst du auf einen Blick, welcher Bereich der App betroffen ist. Bei TypeScript-Setups lohnt sich eine zentrale Channel-Map mit Typen.

Interessantes

ipcRenderer NIE direkt im Renderer.

Mit contextIsolation: true ist es da gar nicht zugänglich. Mit contextIsolation: false würde es funktionieren — aber auch von kompromittiertem Web-Code aus. Immer über Preload + contextBridge.

Pro Channel ein Handler — keine Mehrfach-Registrierung bei handle.

ipcMain.handle('foo', ...) zweimal aufzurufen wirft einen Fehler. ipcMain.on('foo', ...) darf hingegen mehrere Listener haben — Event-basiert, alle werden gerufen.

event.sender für gezielte Antworten.

Bei Multi-Window-Apps unterscheiden, woher der Call kam. event.sender.send(...) antwortet nur dem ursprünglichen Renderer. BrowserWindow.getAllWindows().forEach(w => w.webContents.send(...)) würde an alle broadcasten.

Fehler in Handlern werden zur Promise-Rejection.

Wenn ein handle-Handler eine Exception wirft, lehnt der Renderer-invoke mit derselben Fehlermeldung ab. Damit funktioniert try/catch im Renderer wie erwartet — sehr saubere Error-Propagation.

Argumente werden Structured-Clone'd.

Was du mitgibst, muss serialisierbar sein. Plain-Objects, Arrays, Strings, Numbers — alles okay. Funktionen, Klassen-Instanzen mit Methoden, DOM-Knoten — nein. Buffer und Date funktionieren.

Channel-Map als Single Source of Truth.

Bei wachsenden Apps lohnt eine zentrale Datei mit allen Channel-Konstanten plus TypeScript-Typen für Argumente und Returns. Verhindert Tippfehler und macht Refactorings sicher.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu IPC

Zur Übersicht