Electron – Setting up IPC communication between main and renderer processes

I am using https://github.com/maxogden/menubar to create a menubar desktop application with Electron. However, I am struggling to get basic IPC communication set up. Any idea why the below code doesn’t work? To clarify on that note, I am expecting test to be logged out onto the console when the application fires up, but it isn’t.


const { app } = require('electron');
const Menubar = require('menubar');

const menubar = Menubar.menubar({
  index: `file://${__dirname}/index.html`,
  preloadWindow: true,
  icon: './assets/img/icon.png',

try {
} catch (_) { }

app.on('ready', () => {


const { ipcRenderer } = require('electron');

ipcRenderer.on('test', () => {




This may be a false negative.

I am expecting test to be logged out onto the console when the application fires up, but it isn’t.

The output location of console.log calls depends on where those calls were made:

  • From the main thread: see the terminal that launched the app.

  • From the renderer thread: see the Chrome DevTools console.

So please make sure you were looking for the expected output at the right place.

If that didn’t work then here’s a little demo on how to setup IPC communication between the main and renderer processes.


You’ll notice that I did set up both nodeIntegration and contextIsolation to their default value. This was done on purpose to make it clear that you do not need to lower the security bar of your app to allow messages between your main and renderer processes.

What’s going on here?

The main process waits until the renderer has finished loading before sending its “ping” message. The IPC communication will be handled by the preload script.

Note the console.log call and see where it appears in the screencast below.

const {app, BrowserWindow} = require('electron'); // <-- v15
const path = require('path');

app.whenReady().then(() => {
  const win = new BrowserWindow({
    webPreferences: {
      devTools: true,
      preload: path.resolve(__dirname, 'preload.js'),
      nodeIntegration: false, // <-- This is the default value
      contextIsolation: true  // <-- This is the default value
  win.webContents.on('did-finish-load', () => {
    win.webContents.send('ping', '🏓');
  // This will not show up in the Chrome DevTools Console
  // This will show up in the terminal that launched the app
  console.log('this is from the main thread');


We’re using the contextBridge API. This allows to expose a privileged API to your renderer process without enabling nodeIntegration or breaking context isolation.

The API will be available under a very silly namespace (BURRITO) to make it clear that you can change this.

const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('BURRITO', {
  whenPing: () => new Promise((res) => {
    ipcRenderer.on('ping', (ev, data) => {


Using the API provided by the preload script we start listening for the ping message. When we do get it we put the data communicated by the main process in the renderer page. We also log a message which you can see in the screencast below.

BURRITO.whenPing().then(data => {
  document.querySelector('div').textContent = data;
  // This will show up in the Chrome DevTools Console
  console.log(`this is the renderer thread, received ${data} from main thread`);


<!DOCTYPE html>
    <meta charset="UTF-8">
    <title>IPC Demo</title>
    <script src="./renderer.js"></script>

Run the app with:

npx electron main.js

You can see that the two console.log calls produced output in two different places.

enter image description here