NexusCS

Electron

JavaScript
Electron is a framework for building cross-platform desktop applications using JavaScript, HTML, and CSS. Build native apps with web technologies.
desktop
node
electron

Getting started

Introduction

Electron combines Chromium and Node.js into a single runtime. Build desktop apps for Windows, macOS, and Linux using web technologies.

Current Version: 40.2.0

Quick Start

# Create new app with Electron Forge
npm init electron-app@latest my-app

# Navigate to app
cd my-app

# Start development
npm start

# Build distributable
npm run make

Minimal App Structure

my-app/
├── package.json
├── main.js          # Main process
├── preload.js       # Preload script
└── index.html       # Renderer UI

Main entry in package.json:

{
  "main": "main.js"
}

Basic main.js

const { app, BrowserWindow } = require("electron");
const path = require("node:path");

function createWindow() {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, "preload.js"),
      contextIsolation: true,
      nodeIntegration: false,
    },
  });

  win.loadFile("index.html");
}

app.whenReady().then(() => {
  createWindow();

  app.on("activate", () => {
    if (BrowserWindow.getAllWindows().length === 0) {
      createWindow();
    }
  });
});

app.on("window-all-closed", () => {
  if (process.platform !== "darwin") {
    app.quit();
  }
});

Process Architecture

Main Process

Single process that runs in Node.js environment. Controls app lifecycle and creates renderer processes.

// main.js - Main process entry point
const { app, BrowserWindow } = require("electron");

app.whenReady().then(() => {
  // Create windows, setup IPC, etc.
});

Capabilities:

  • Full Node.js API access
  • Create BrowserWindows
  • Handle IPC communication
  • Access native OS features
  • Control app lifecycle

Renderer Process

One per BrowserWindow. Runs in Chromium environment with web standards (HTML/CSS/JS).

<!-- index.html - Renderer process -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>My App</title>
  </head>
  <body>
    <h1>Hello Electron!</h1>
    <script src="./renderer.js"></script>
  </body>
</html>

Capabilities:

  • DOM manipulation
  • Web APIs (fetch, localStorage, etc.)
  • Limited Node.js access (via preload)
  • Isolated from main process

Preload Scripts

Runs before renderer, bridges main and renderer with controlled API exposure.

// preload.js
const { contextBridge, ipcRenderer } = require("electron");

contextBridge.exposeInMainWorld("electronAPI", {
  sendMessage: (channel, data) => {
    ipcRenderer.send(channel, data);
  },
  onReply: (callback) => {
    ipcRenderer.on("reply", (_event, value) => callback(value));
  },
});

Purpose:

  • Expose safe APIs to renderer
  • Enforce security boundaries
  • Validate IPC messages

IPC Communication

Renderer → Main (One-way)

// preload.js
const { contextBridge, ipcRenderer } = require("electron");

contextBridge.exposeInMainWorld("electronAPI", {
  send: (channel, data) => ipcRenderer.send(channel, data),
});
// main.js
const { ipcMain } = require("electron");

ipcMain.on("message-channel", (event, data) => {
  console.log("Received:", data);
});
// renderer.js
window.electronAPI.send("message-channel", {
  text: "Hello from renderer",
});

Renderer → Main (Two-way)

// preload.js
contextBridge.exposeInMainWorld("electronAPI", {
  invoke: (channel, data) => ipcRenderer.invoke(channel, data),
});
// main.js
ipcMain.handle("get-data", async (event, arg) => {
  const result = await fetchData(arg);
  return result;
});
// renderer.js
const data = await window.electronAPI.invoke("get-data", "param");
console.log(data);

Main → Renderer

// main.js
const { BrowserWindow } = require("electron");

const win = BrowserWindow.getFocusedWindow();
win.webContents.send("update", { data: "New data" });
// preload.js
contextBridge.exposeInMainWorld("electronAPI", {
  onUpdate: (callback) => {
    ipcRenderer.on("update", (_event, value) => callback(value));
  },
});
// renderer.js
window.electronAPI.onUpdate((data) => {
  console.log("Update received:", data);
});

IPC Channel Validation

// preload.js - Whitelist channels
const VALID_CHANNELS = ["message", "data-request"];

contextBridge.exposeInMainWorld("electronAPI", {
  send: (channel, data) => {
    if (VALID_CHANNELS.includes(channel)) {
      ipcRenderer.send(channel, data);
    }
  },
});
// main.js - Validate sender
ipcMain.on("message", (event, data) => {
  // Verify sender is from expected window
  if (event.senderFrame === mainWindow.webContents.mainFrame) {
    processMessage(data);
  }
});

BrowserWindow

Window Creation

const { BrowserWindow } = require("electron");

const win = new BrowserWindow({
  width: 1024,
  height: 768,
  minWidth: 800,
  minHeight: 600,
  show: false, // Don't show until ready
  webPreferences: {
    preload: path.join(__dirname, "preload.js"),
    contextIsolation: true,
    nodeIntegration: false,
    sandbox: true,
  },
});

win.loadFile("index.html");

Window Events

// Show window when ready
win.once("ready-to-show", () => {
  win.show();
});

// Handle close
win.on("close", (event) => {
  // Prevent close, show confirmation
  event.preventDefault();
  dialog
    .showMessageBox({
      type: "question",
      buttons: ["Yes", "No"],
      message: "Are you sure you want to quit?",
    })
    .then(({ response }) => {
      if (response === 0) win.destroy();
    });
});

// Cleanup on closed
win.on("closed", () => {
  win = null;
});

// Window focus events
win.on("focus", () => console.log("Focused"));
win.on("blur", () => console.log("Blurred"));

Window Methods

// Load content
win.loadFile("index.html");
win.loadURL("https://example.com");

// Window state
win.maximize();
win.minimize();
win.restore();
win.close();

// Window properties
win.setTitle("New Title");
win.setSize(800, 600);
win.center();

// Developer tools
win.webContents.openDevTools();
win.webContents.closeDevTools();

Frameless Window

const win = new BrowserWindow({
  width: 800,
  height: 600,
  frame: false,
  titleBarStyle: "hidden", // macOS only
  transparent: true,
});

Custom titlebar with draggable region:

/* styles.css */
.titlebar {
  -webkit-app-region: drag;
  height: 32px;
}

.titlebar button {
  -webkit-app-region: no-drag;
}

Security

Security Checklist

✅ MUST Enable:

webPreferences: {
  contextIsolation: true,      // ✅ Isolate contexts
  sandbox: true,               // ✅ Enable sandbox
  nodeIntegration: false,      // ✅ Disable node in renderer
  enableRemoteModule: false    // ✅ Disable remote
}

❌ MUST Disable:

webPreferences: {
  nodeIntegration: true,       // ❌ NEVER for remote content
  webSecurity: false,          // ❌ NEVER disable
  allowRunningInsecureContent: true  // ❌ NEVER allow
}

Content Security Policy:

<meta
  http-equiv="Content-Security-Policy"
  content="default-src 'self'; script-src 'self'"
/>

Validate All Inputs

// ❌ BAD - No validation
ipcMain.handle("open-file", async (event, filePath) => {
  return fs.readFileSync(filePath);
});

// ✅ GOOD - Validate path
ipcMain.handle("open-file", async (event, filePath) => {
  const safeDir = app.getPath("userData");
  const resolvedPath = path.resolve(safeDir, filePath);

  if (!resolvedPath.startsWith(safeDir)) {
    throw new Error("Invalid path");
  }

  return fs.readFileSync(resolvedPath);
});

Secure URL Loading

// ❌ BAD - User input directly
win.loadURL(userInput);

// ✅ GOOD - Validate URLs
const { URL } = require("url");

function loadSafeURL(urlString) {
  try {
    const url = new URL(urlString);

    // Only allow HTTPS
    if (url.protocol !== "https:") {
      throw new Error("Only HTTPS allowed");
    }

    // Whitelist domains
    const allowedHosts = ["example.com", "api.example.com"];
    if (!allowedHosts.includes(url.hostname)) {
      throw new Error("Domain not allowed");
    }

    win.loadURL(url.toString());
  } catch (err) {
    console.error("Invalid URL:", err);
  }
}

Navigation Protection

// Prevent navigation to untrusted sites
win.webContents.on("will-navigate", (event, url) => {
  const { URL } = require("url");
  const parsedUrl = new URL(url);

  if (parsedUrl.origin !== "https://example.com") {
    event.preventDefault();
  }
});

// Prevent new window creation
win.webContents.setWindowOpenHandler(({ url }) => {
  // Open in default browser instead
  shell.openExternal(url);
  return { action: "deny" };
});

Native Dialogs

File Open Dialog

const { dialog } = require("electron");

// Single file
const result = await dialog.showOpenDialog({
  properties: ["openFile"],
  filters: [
    { name: "Images", extensions: ["jpg", "png", "gif"] },
    { name: "All Files", extensions: ["*"] },
  ],
});

if (!result.canceled) {
  console.log(result.filePaths[0]);
}
// Multiple files
const result = await dialog.showOpenDialog({
  properties: ["openFile", "multiSelections"],
});

// Directory selection
const result = await dialog.showOpenDialog({
  properties: ["openDirectory"],
});

Save Dialog

const result = await dialog.showSaveDialog({
  title: "Save File",
  defaultPath: "document.txt",
  filters: [
    { name: "Text Files", extensions: ["txt"] },
    { name: "All Files", extensions: ["*"] },
  ],
});

if (!result.canceled) {
  fs.writeFileSync(result.filePath, content);
}

Message Box

// Info dialog
await dialog.showMessageBox({
  type: "info",
  title: "Information",
  message: "Operation completed",
  detail: "Additional details here",
});

// Confirmation dialog
const result = await dialog.showMessageBox({
  type: "question",
  buttons: ["Yes", "No", "Cancel"],
  defaultId: 0,
  cancelId: 2,
  message: "Save changes?",
  detail: "You have unsaved changes",
});

if (result.response === 0) {
  // Yes clicked
}

Error Box

// Synchronous error dialog
dialog.showErrorBox("Error Title", "Error message details");

Types: none, info, error, question, warning

Menu & Tray

Application Menu

const { Menu, app } = require("electron");

const template = [
  {
    label: "File",
    submenu: [
      {
        label: "New",
        accelerator: "CmdOrCtrl+N",
        click: () => createNewFile(),
      },
      {
        label: "Open",
        accelerator: "CmdOrCtrl+O",
        click: () => openFile(),
      },
      { type: "separator" },
      { role: "quit" },
    ],
  },
  {
    label: "Edit",
    submenu: [
      { role: "undo" },
      { role: "redo" },
      { type: "separator" },
      { role: "cut" },
      { role: "copy" },
      { role: "paste" },
    ],
  },
  {
    label: "View",
    submenu: [
      { role: "reload" },
      { role: "toggleDevTools" },
      { type: "separator" },
      { role: "resetZoom" },
      { role: "zoomIn" },
      { role: "zoomOut" },
    ],
  },
];

const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);

Context Menu

// preload.js
const { contextBridge, ipcRenderer } = require("electron");

contextBridge.exposeInMainWorld("electronAPI", {
  showContextMenu: () => ipcRenderer.send("show-context-menu"),
});

// main.js
const { Menu, ipcMain } = require("electron");

const contextMenu = Menu.buildFromTemplate([
  { label: "Copy", role: "copy" },
  { label: "Paste", role: "paste" },
  { type: "separator" },
  { label: "Custom Action", click: () => console.log("Clicked") },
]);

ipcMain.on("show-context-menu", (event) => {
  contextMenu.popup(BrowserWindow.fromWebContents(event.sender));
});

// renderer.js
window.addEventListener("contextmenu", (e) => {
  e.preventDefault();
  window.electronAPI.showContextMenu();
});

System Tray

const { Tray, Menu, nativeImage } = require("electron");

let tray = null;

app.whenReady().then(() => {
  const icon = nativeImage.createFromPath("assets/icon.png");
  tray = new Tray(icon.resize({ width: 16, height: 16 }));

  const contextMenu = Menu.buildFromTemplate([
    { label: "Show App", click: () => mainWindow.show() },
    { type: "separator" },
    { label: "Quit", click: () => app.quit() },
  ]);

  tray.setToolTip("My Electron App");
  tray.setContextMenu(contextMenu);

  tray.on("click", () => {
    mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show();
  });
});

Menu Roles

Built-in roles for common actions:

{
  role: "undo";
}
{
  role: "redo";
}
{
  role: "cut";
}
{
  role: "copy";
}
{
  role: "paste";
}
{
  role: "selectAll";
}
{
  role: "reload";
}
{
  role: "toggleDevTools";
}
{
  role: "quit";
}
{
  role: "minimize";
}
{
  role: "close";
}
{
  role: "help";
}
{
  role: "about";
}
{
  role: "services";
} // macOS only
{
  role: "hide";
} // macOS only

App Lifecycle

Lifecycle Events

const { app } = require("electron");

// App is ready to create windows
app.on("ready", () => {
  console.log("App ready");
});

// Alternative to 'ready'
app.whenReady().then(() => {
  console.log("App ready (promise)");
});

// All windows closed
app.on("window-all-closed", () => {
  // Quit on all platforms except macOS
  if (process.platform !== "darwin") {
    app.quit();
  }
});

// macOS: Re-activate (dock icon clicked)
app.on("activate", () => {
  if (BrowserWindow.getAllWindows().length === 0) {
    createWindow();
  }
});

// Before app quits
app.on("before-quit", (event) => {
  // Cleanup, save state
  console.log("Before quit");
});

// App will quit
app.on("will-quit", (event) => {
  // Final cleanup
  console.log("Will quit");
});

// App quit
app.on("quit", (event, exitCode) => {
  console.log("Quit with code:", exitCode);
});

Preventing Quit

// Prevent quit on close
app.on("before-quit", (event) => {
  if (!readyToQuit) {
    event.preventDefault();

    // Perform async cleanup
    performCleanup().then(() => {
      readyToQuit = true;
      app.quit();
    });
  }
});

Single Instance Lock

const gotTheLock = app.requestSingleInstanceLock();

if (!gotTheLock) {
  app.quit();
} else {
  app.on("second-instance", (event, argv, workingDir) => {
    // Focus existing window
    if (mainWindow) {
      if (mainWindow.isMinimized()) mainWindow.restore();
      mainWindow.focus();
    }
  });

  app.whenReady().then(() => {
    createWindow();
  });
}

App Paths

// Get common paths
app.getPath("home"); // User's home
app.getPath("appData"); // App data
app.getPath("userData"); // User data for app
app.getPath("temp"); // Temp directory
app.getPath("downloads"); // Downloads
app.getPath("documents"); // Documents
app.getPath("desktop"); // Desktop
app.getPath("logs"); // Logs

// Set custom user data path
app.setPath("userData", "/path/to/custom/dir");

Building & Packaging

Electron Forge Setup

# Create new app
npm init electron-app@latest my-app

# With template
npm init electron-app@latest my-app -- \
  --template=webpack

# Available templates
# - webpack
# - webpack-typescript
# - vite
# - vite-typescript

Project Scripts

{
  "scripts": {
    "start": "electron-forge start",
    "package": "electron-forge package",
    "make": "electron-forge make",
    "publish": "electron-forge publish"
  }
}
# Development
npm start

# Create distributable
npm run make

# Publish to distribution service
npm run publish

Forge Configuration

// forge.config.js
module.exports = {
  packagerConfig: {
    asar: true,
    icon: "./assets/icon",
    appBundleId: "com.myapp.id",
    appCategoryType: "public.app-category.utilities",
  },
  makers: [
    {
      name: "@electron-forge/maker-squirrel",
      config: {
        // Windows installer config
      },
    },
    {
      name: "@electron-forge/maker-dmg",
      config: {
        // macOS DMG config
      },
    },
    {
      name: "@electron-forge/maker-deb",
      config: {
        // Linux Debian config
      },
    },
  ],
};

Auto-Update Setup

const { autoUpdater } = require("electron-updater");

app.whenReady().then(() => {
  // Check for updates on startup
  autoUpdater.checkForUpdatesAndNotify();

  // Check periodically
  setInterval(() => {
    autoUpdater.checkForUpdatesAndNotify();
  }, 60000 * 60); // Every hour
});

autoUpdater.on("update-available", () => {
  console.log("Update available");
});

autoUpdater.on("update-downloaded", () => {
  dialog
    .showMessageBox({
      type: "info",
      title: "Update Ready",
      message: "A new version is ready. Restart now?",
      buttons: ["Restart", "Later"],
    })
    .then((result) => {
      if (result.response === 0) {
        autoUpdater.quitAndInstall();
      }
    });
});

Debugging

Renderer DevTools

// Open DevTools programmatically
mainWindow.webContents.openDevTools();

// Open in detached mode
mainWindow.webContents.openDevTools({ mode: "detach" });

// Auto-open on window creation
const win = new BrowserWindow({
  webPreferences: {
    devTools: true,
  },
});
win.webContents.openDevTools();

Keyboard shortcut: Cmd+Option+I (macOS) or Ctrl+Shift+I (Windows/Linux)

Main Process Debugging

# Start with inspector
electron --inspect=5858 .

# Break on first line
electron --inspect-brk=5858 .

Connect Chrome DevTools:

  1. Open chrome://inspect in Chrome
  2. Click "Configure" → Add localhost:5858
  3. Click "inspect" under Remote Target

VSCode Configuration

// .vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Debug Main Process",
      "type": "node",
      "request": "launch",
      "cwd": "${workspaceFolder}",
      "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
      "windows": {
        "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
      },
      "args": ["."],
      "outputCapture": "std"
    },
    {
      "name": "Debug Renderer Process",
      "type": "chrome",
      "request": "launch",
      "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
      "windows": {
        "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
      },
      "runtimeArgs": [".", "--remote-debugging-port=9223"],
      "webRoot": "${workspaceFolder}"
    }
  ]
}

Logging & Debug Output

// Enable Electron debug logs
app.commandLine.appendSwitch("enable-logging");
app.commandLine.appendSwitch("v", "1");

// Custom logging
const log = require("electron-log");

log.info("Info message");
log.warn("Warning message");
log.error("Error message");

// Logs location
console.log(log.transports.file.getFile());

Common Patterns

Loading Screen

let mainWindow;
let loadingWindow;

function createLoadingWindow() {
  loadingWindow = new BrowserWindow({
    width: 300,
    height: 400,
    frame: false,
    transparent: true,
  });

  loadingWindow.loadFile("loading.html");
}

function createMainWindow() {
  mainWindow = new BrowserWindow({
    width: 1024,
    height: 768,
    show: false,
  });

  mainWindow.loadFile("index.html");

  mainWindow.once("ready-to-show", () => {
    loadingWindow.close();
    mainWindow.show();
  });
}

app.whenReady().then(() => {
  createLoadingWindow();

  setTimeout(() => {
    createMainWindow();
  }, 1000);
});

Store User Data

const Store = require("electron-store");

const store = new Store();

// Set values
store.set("preferences.theme", "dark");
store.set("windowBounds", { width: 800, height: 600 });

// Get values
const theme = store.get("preferences.theme");
const bounds = store.get("windowBounds", { width: 800, height: 600 });

// Delete
store.delete("preferences.theme");

// Clear all
store.clear();

// Get store path
console.log(store.path);

Deep Linking

// Protocol registration (macOS/Windows)
if (process.defaultApp) {
  if (process.argv.length >= 2) {
    app.setAsDefaultProtocolClient("myapp", process.execPath, [
      path.resolve(process.argv[1]),
    ]);
  }
} else {
  app.setAsDefaultProtocolClient("myapp");
}

// Handle deep link
const gotTheLock = app.requestSingleInstanceLock();

if (!gotTheLock) {
  app.quit();
} else {
  app.on("second-instance", (event, argv, workingDir) => {
    // Windows/Linux deep link
    const url = argv.find((arg) => arg.startsWith("myapp://"));
    if (url) handleDeepLink(url);

    if (mainWindow) {
      if (mainWindow.isMinimized()) mainWindow.restore();
      mainWindow.focus();
    }
  });

  // macOS deep link
  app.on("open-url", (event, url) => {
    event.preventDefault();
    handleDeepLink(url);
  });
}

function handleDeepLink(url) {
  console.log("Deep link:", url);
  // myapp://action/param
}

Native Notifications

// Main process
const { Notification } = require("electron");

function showNotification(title, body) {
  new Notification({
    title: title,
    body: body,
    icon: path.join(__dirname, "icon.png"),
  }).show();
}

// Renderer process (HTML5 Notification API)
if (Notification.permission === "granted") {
  new Notification("Title", {
    body: "Message body",
    icon: "icon.png",
  });
} else if (Notification.permission !== "denied") {
  Notification.requestPermission().then((permission) => {
    if (permission === "granted") {
      new Notification("Title", { body: "Message" });
    }
  });
}

Global Shortcuts

const { globalShortcut } = require("electron");

app.whenReady().then(() => {
  // Register shortcut
  globalShortcut.register("CommandOrControl+X", () => {
    console.log("CommandOrControl+X pressed");
    mainWindow.show();
  });

  // Check if registered
  const isRegistered = globalShortcut.isRegistered("CommandOrControl+X");
  console.log("Registered:", isRegistered);
});

app.on("will-quit", () => {
  // Unregister specific shortcut
  globalShortcut.unregister("CommandOrControl+X");

  // Unregister all shortcuts
  globalShortcut.unregisterAll();
});

Advanced Features

Screen Capture

const { desktopCapturer } = require("electron");

async function getScreenSources() {
  const sources = await desktopCapturer.getSources({
    types: ["window", "screen"],
    thumbnailSize: { width: 150, height: 150 },
  });

  return sources.map((source) => ({
    id: source.id,
    name: source.name,
    thumbnail: source.thumbnail.toDataURL(),
  }));
}

// In renderer (with preload bridge)
const sources = await window.electronAPI.getScreenSources();

sources.forEach((source) => {
  console.log(source.name);
});

Power Management

const { powerMonitor, powerSaveBlocker } = require("electron");

app.whenReady().then(() => {
  // Monitor power events
  powerMonitor.on("suspend", () => {
    console.log("System going to sleep");
  });

  powerMonitor.on("resume", () => {
    console.log("System woke up");
  });

  powerMonitor.on("on-ac", () => {
    console.log("Plugged in");
  });

  powerMonitor.on("on-battery", () => {
    console.log("On battery");
  });

  // Prevent sleep
  const id = powerSaveBlocker.start("prevent-app-suspension");

  // Later, allow sleep
  powerSaveBlocker.stop(id);
});

Print to PDF

// Main process
ipcMain.handle("print-pdf", async (event, options) => {
  const win = BrowserWindow.fromWebContents(event.sender);

  const data = await win.webContents.printToPDF({
    marginsType: 0,
    pageSize: "A4",
    printBackground: true,
    landscape: false,
  });

  const pdfPath = path.join(app.getPath("downloads"), "output.pdf");
  fs.writeFileSync(pdfPath, data);

  return pdfPath;
});

// Renderer
const pdfPath = await window.electronAPI.printPDF();
console.log("PDF saved to:", pdfPath);

Custom Protocol

const { protocol } = require("electron");

app.whenReady().then(() => {
  protocol.handle("myapp", (request) => {
    const url = request.url.substr(8); // Remove 'myapp://'
    const filePath = path.join(__dirname, "assets", url);

    return net.fetch("file://" + filePath);
  });
});

// Use in renderer
win.loadURL("myapp://index.html");

Gotchas

macOS App Activation

On macOS, apps stay active even when all windows are closed. Always check for zero windows on activate event:

app.on("activate", () => {
  if (BrowserWindow.getAllWindows().length === 0) {
    createWindow();
  }
});

Context Isolation Breaking Changes

Since Electron 12, contextIsolation: true is default. Direct require() in renderer no longer works:

// ❌ No longer works (even with nodeIntegration: true)
const fs = require("fs"); // Error in renderer

// ✅ Use preload + contextBridge
// preload.js
contextBridge.exposeInMainWorld("fs", {
  readFile: (path) => fs.readFileSync(path, "utf-8"),
});

Remote Module Deprecated

@electron/remote is deprecated. Use IPC instead:

// ❌ Old way (deprecated)
const { BrowserWindow } = require("@electron/remote");
const win = new BrowserWindow();

// ✅ New way (IPC)
// preload.js
contextBridge.exposeInMainWorld("electronAPI", {
  createWindow: () => ipcRenderer.invoke("create-window"),
});

// main.js
ipcMain.handle("create-window", () => {
  const win = new BrowserWindow({ width: 800, height: 600 });
});

ASAR Archive Path Issues

When packaged, use __dirname carefully with asar archives:

// ❌ May fail in production
const iconPath = __dirname + "/icon.png";

// ✅ Use path.join
const iconPath = path.join(__dirname, "icon.png");

// ✅ Or use process.resourcesPath for static assets
const iconPath = path.join(process.resourcesPath, "icon.png");

Ready Event Timing

Don't create windows before ready event:

// ❌ Too early
const win = new BrowserWindow(); // Error

app.whenReady().then(() => {
  // ✅ Safe to create windows
  const win = new BrowserWindow();
});

Also see