# electron-better-ipc adds request and reply to Electron IPC

> Electron's built-in ipcMain.handle and ipcRenderer.invoke already cover renderer to main, so this small module earns its place by filling the three gaps: main to renderer, renderer to renderer, and errors that survive the trip.

**sindresorhus/electron-better-ipc** — Simplified IPC communication for Electron apps

- Repository: https://github.com/sindresorhus/electron-better-ipc
- Stars: 747 · Forks: 53
- Language: JavaScript
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/sindresorhus-electron-better-ipc

## The module is explicit about what the built-in IPC already does

The readme opens with a section titled Do you need this module, and the answer is more nuanced than a feature list. The built-in ipcMain.handle and ipcRenderer.invoke already send a message and get the response back in the same call, and the instruction is to use them for the renderer to main direction. The module is then for what the built-in IPC does not do. That framing matters for adoption, because the first question a team should ask is which of its channels actually need the extra machinery. The list of gaps is four items. Main to renderer, where you send a message from the main process to a renderer and await the reply, something webContents.send cannot do, and where the call rejects when the renderer has no handler for the channel or when the page closes, reloads or crashes before it answers. Renderer to renderer, by web contents ID, awaiting the reply, which works for windows and for WebContentsView pages. Complete errors, since when a handle listener throws the renderer gets an error with only the message property preserved, and this module uses serialize-error so the whole error object survives in both directions. And the BrowserWindow that sent the message is passed to your callback, with the answer methods returning a function that removes the handler.

## Two directions, two pairs of function names

The API is two objects, ipcMain and ipcRenderer, each with answer and call variants for the direction in question. For renderer to main, the main process registers a handler with ipcMain.answerRenderer and the renderer calls ipcRenderer.callMain.

```js
import {ipcMain as ipc} from 'electron-better-ipc';

ipc.answerRenderer('get-emoji', async (emojiName, browserWindow) => {
	return getEmoji(emojiName);
});
```

```js
import {ipcRenderer as ipc} from 'electron-better-ipc';

const emoji = await ipc.callMain('get-emoji', 'unicorn');
console.log(emoji);
//=> '🦄'
```

For main to renderer, which the readme says the built-in IPC has no equal for, since webContents.send cannot wait for a reply, the pair is ipcMain.callRenderer and ipcRenderer.answerMain. There is also ipcMain.callFocusedRenderer, which targets the focused window rather than a named one, and ipcMain.callRenderer takes a target, a channel and optional data so you can address one specific window or the web contents of a WebContentsView. One constraint on the answer side is worth knowing before you write the handler: answerRenderer registers an ipcMain.handle handler on the plain channel name, so only one handler can exist per channel and registering the same channel twice throws. The function it returns removes the handler.

## Renderer to renderer is keyed on web contents, not window

The third direction is the least obvious one. ipcRenderer.callRenderer and ipcRenderer.answerRenderer cover it, and the main process passes the call on to the target page, so the target page must import the module too, and the target page then replies to the caller directly. Pages are identified by their WebContents id, for example browserWindow.webContents.id or view.webContents.id, and the readme stresses that this is not the BrowserWindow id. A renderer cannot get those ids by itself, so they have to come from the main process, which is why the example starts with a main-side handler returning editorWindow.webContents.id.

```js
import {ipcMain as ipc} from 'electron-better-ipc';

ipc.answerRenderer('get-editor-id', () => editorWindow.webContents.id);
```

The renderer then calls the other page directly.

```js
import {ipcRenderer as ipc} from 'electron-better-ipc';

const editorId = await ipc.callMain('get-editor-id');

const text = await ipc.callRenderer(editorId, 'get-selected-text');
```

And the target page answers, receiving the id of the caller.

```js
ipc.answerRenderer('get-selected-text', (data, webContentsId) => {
	return document.getSelection().toString();
});
```

There is a security detail in that last pair. answerRenderer and answerMain use separate channels, so a renderer cannot call an answerMain handler in another page, which stops a page from reaching sideways into a privileged handler.

## Context isolation means the module belongs in the preload script

With context isolation, which is the Electron default, the web page cannot import the module at all. The instruction is to use it in the preload script and expose only the functions the page needs with contextBridge, and the warning is not to expose the IPC objects themselves, since the page could then call any channel.

```js
// preload.mjs
import {contextBridge} from 'electron';
import {ipcRenderer as ipc} from 'electron-better-ipc';

contextBridge.exposeInMainWorld('app', {
	getEmoji: emojiName => ipc.callMain('get-emoji', emojiName),
});

ipc.answerMain('get-title', () => document.title);
```

The page then calls the narrow wrapper rather than the IPC surface.

```js
// In the web page
const emoji = await window.app.getEmoji('unicorn');
```

The installation section has a related trap that will cost you an afternoon if you miss it. The module has to be imported in the main process and in the preload script of each window that uses it. If you bundle the preload script, with electron-vite or webpack for example, it works with the default sandbox. Without a bundler, the preload script must use sandbox: false, since a sandboxed preload cannot load npm modules, and it must have the .mjs extension, because preload scripts ignore the type: module field in package.json.

## Typed channels are a single shared type

For TypeScript users the readme describes a typing scheme that is worth adopting whatever you do about the runtime side. You use TypedMainProcessIpc and TypedRendererProcessIpc to get strictly typed channels, and you describe all channels in one type, in up to three groups, shared between the main and renderer code. Each channel is described as a method taking at most one parameter, the data. The three groups map to the directions already described. The main group is the channels the main process answers, through ipcMain.answerRenderer and ipcRenderer.callMain. The renderer group is the channels a renderer process answers for the main process, through ipcRenderer.answerMain and ipcMain.callRenderer. And rendererToRenderer is the channels a renderer process answers for other renderer processes, through ipcRenderer.answerRenderer and ipcRenderer.callRenderer. You leave out a group that has no channels, and the readme notes that answer registrations require a single channel key, so narrow union channels before registering them.

## A small package with a real constraint behind it

The packaging tells you what kind of project this is. It is an ES module, so you load it with import, and it requires Electron 44 or later and Node 22 or newer. It has exactly one runtime dependency, serialize-error at 13.0.1, which is the whole error preservation mechanism, and the side effects field lists the main and renderer source files, so a bundler that tree shakes will keep the side-effectful parts. The test story is xo for linting and tsd for the type tests, with a separate test-with-app script that also runs the fixture app with the node test runner, and the xo configuration ignores the fixture directory because it is an app rather than library code. The version history is the other useful signal, since v2.0.0 and v2.0.1 shipped in 2021 and v3.0.0 shipped on 2026-10-07, a long gap that suggests the Electron requirement is what moved rather than the feature set.

## Conclusion

electron-better-ipc suits an Electron team that has outgrown fire and forget messaging and wants promises in both directions, and it suits anyone who has been bitten by an Electron error arriving at the renderer with only its message property intact. It is the wrong choice for a one-way notification system, since the built-in webContents.send already covers that, and it is the wrong choice if you cannot move to Electron 44 or later, which the package requires. Before adopting it, decide which direction each of your channels really needs, because the module is explicit that the built-in IPC remains the right choice for renderer to main in most cases, and read the section on context isolation before you expose anything to a page. The last push was on 2026-10-07 and the current release is v3.0.0 from the same day.

## FAQ

### When should I use electron-better-ipc instead of the built-in Electron IPC?

The README says to use the built-in ipcMain.handle and ipcRenderer.invoke for the renderer to main direction, and this module for what the built-in IPC does not do: main to renderer with an awaited reply, renderer to renderer, complete error objects, and access to the BrowserWindow that sent the message.

### How do I send a message from the main process to a renderer with electron-better-ipc?

Use ipcMain.callRenderer to target one specific window or the web contents of a WebContentsView, or ipcMain.callFocusedRenderer for the focused one, and have the renderer register the reply with ipcRenderer.answerMain. The built-in webContents.send cannot wait for a reply.

### What are the preload script requirements for electron-better-ipc?

Import the module in the preload script of each window that uses it. If you bundle the preload script, for example with electron-vite or webpack, it works with the default sandbox. Without a bundler, the preload script must use sandbox: false because a sandboxed preload cannot load npm modules, and it must have the .mjs extension because preload scripts ignore the type module field.

## Sources

- [Issues](https://github.com/sindresorhus/electron-better-ipc/issues)
- [License: MIT](https://github.com/sindresorhus/electron-better-ipc/blob/main/LICENSE)
- [README](https://github.com/sindresorhus/electron-better-ipc/blob/main/README.md)
- [Releases](https://github.com/sindresorhus/electron-better-ipc/releases)
- [sindresorhus/electron-better-ipc on GitHub](https://github.com/sindresorhus/electron-better-ipc)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/sindresorhus-electron-better-ipc
