send und on sind die ältere, Event-basierte IPC-Variante. Anders als invoke/handle gibt es keinen direkten Return-Value — was sie für Fire-and-Forget und vor allem für Push-Events vom Main an den Renderer ideal macht. Plus eine wichtige Warnung: sendSync ist quasi nie die richtige Wahl.

Renderer → Main (Fire-and-Forget)

JavaScript renderer / preload
// Preload exposed:
contextBridge.exposeInMainWorld('analytics', {
    track: (event, payload) => ipcRenderer.send('analytics:track', event, payload)
});

// Renderer:
window.analytics.track('button_click', { id: 'submit' });
JavaScript main.js
ipcMain.on('analytics:track', (_event, name, payload) => {
    sendToAnalyticsBackend(name, payload);
    // Kein Return-Wert — Renderer hat schon weitergemacht
});

send returned sofort. Der Renderer bekommt keine Bestätigung, kein Resultat. Für Telemetrie, Logging, „Status-Update an den Main" perfekt.

Main → Renderer (Push-Events)

Andersherum — vom Main an den Renderer pushen — ist DIE Hauptanwendung von send/on:

JavaScript main.js
// Beim Download-Progress an Renderer pushen
downloader.on('progress', (percent) => {
    mainWindow.webContents.send('download:progress', { percent });
});

// An ALLE Renderer broadcasten
nativeTheme.on('updated', () => {
    BrowserWindow.getAllWindows().forEach(win => {
        win.webContents.send('theme:changed', nativeTheme.shouldUseDarkColors);
    });
});

webContents.send(channel, ...args) ist das Pendant zu ipcRenderer.send. Der Renderer hört mit ipcRenderer.on (über Preload).

JavaScript preload.js
contextBridge.exposeInMainWorld('api', {
    onDownloadProgress: (callback) => {
        const listener = (_event, data) => callback(data);
        ipcRenderer.on('download:progress', listener);
        return () => ipcRenderer.removeListener('download:progress', listener);
    }
});
JavaScript renderer.js
const unsubscribe = window.api.onDownloadProgress(({ percent }) => {
    updateProgressBar(percent);
});

// Cleanup beim Unmount
unsubscribe();

Antwort über zweiten Channel

Wer mit send/on doch eine Antwort braucht — älteres Pattern (heute idR durch invoke/handle ersetzt):

JavaScript main.js
ipcMain.on('files:read-request', async (event, path) => {
    const content = await fs.readFile(path, 'utf-8');
    event.sender.send('files:read-response', { path, content });
});
JavaScript renderer.js
ipcRenderer.send('files:read-request', '/path/to/file');
ipcRenderer.once('files:read-response', (_event, { content }) => {
    console.log(content);
});

Funktioniert, ist aber umständlich — invoke/handle macht das in einer Zeile. Heute nur noch in Legacy-Code zu sehen.

sendSync — fast immer falsch

JavaScript renderer.js — VORSICHT
// Synchroner Call — BLOCKIERT den Renderer
const result = ipcRenderer.sendSync('app:get-version');
JavaScript main.js — entsprechender Handler
ipcMain.on('app:get-version', (event) => {
    event.returnValue = app.getVersion();
});

Während der Main antwortet, friert der Renderer ein — UI reagiert nicht, Animationen stoppen. Bei langsamen Operationen merkt der User das sofort.

Pragmatisch:

  • Niemals sendSync für Operationen, die mehr als ein paar Mikrosekunden brauchen
  • Sehr selten für super-schnelle Lookups beim App-Start (z. B. initiale Locale)
  • Idiomatisch: immer invoke nutzen

Mehrere Listener pro Channel

Im Gegensatz zu handle darf ipcMain.on mehrere Listener pro Channel haben:

JavaScript
ipcMain.on('app:event', (_event, payload) => {
    console.log('Listener A:', payload);
});

ipcMain.on('app:event', (_event, payload) => {
    saveToDatabase(payload);
});

// Beide werden bei jedem send gerufen

Praktisch für Cross-Cutting-Concerns wie Logging — ein Logger-Listener neben den eigentlichen Business-Listenern.

once für einmalige Events

JavaScript
ipcMain.once('app:initial-config', (_event, config) => {
    applyConfig(config);
    // Nach diesem ersten Aufruf wird der Listener entfernt
});

// Renderer
ipcRenderer.once('app:welcome-shown', () => {
    console.log('Wurde gezeigt');
});

once triggert nur beim ersten Match und entfernt sich dann selbst. Praktisch für Setup-Sequenzen.

FAQ

Wann send/on statt invoke/handle?

Wann immer keine Antwort gebraucht wird — Telemetrie, Logging, Push-Events vom Main. Für Renderer-fragt-Main-mit-Antwort: invoke. Pragmatische Faustregel: webContents.send für Push, invoke für Pull.

Sollte ich sendSync nutzen?

Praktisch nie. Es blockiert den Renderer bis zur Antwort — bei selbst kurzen Operationen UX-störend. Wer denkt „muss synchron sein": fast immer ist es das nicht. Stattdessen invoke mit await.

Wie warte ich auf eine Antwort mit send/on?

Pattern: send plus once-Listener auf einem Reply-Channel. Aber das ist genau das, was invoke/handle einfacher macht — modern fast immer invoke nehmen.

Werden Events bei abgemeldeten Listenern verloren?

Ja — wenn beim webContents.send kein Listener im Renderer aktiv ist, wird das Event verworfen. Pattern: vor dem ersten Push sicherstellen, dass der Renderer ready ist (z. B. über initialen did-finish-load).

Wie broadcaste ich an alle Fenster?

BrowserWindow.getAllWindows().forEach(win => win.webContents.send(channel, data)). Beachten: nur die, deren Renderer auch bereit sind. Wer kompliziertere Broadcasts braucht: eigenen kleinen Pub/Sub im Main bauen.

Cleanup-Pattern für mehrfach instanziierte Komponenten?

Listener bei jeder componentDidMount/onMount registrieren, bei componentWillUnmount/onDestroy wieder ab. Pattern: Subscription-Funktion gibt Unsubscribe-Funktion zurück, die im Cleanup aufgerufen wird.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu IPC

Zur Übersicht