Skip to content

Fix: Electron Not Working — Window Not Showing, IPC Not Communicating, or Build Failing

FixDevs ·

Quick Answer

How to fix Electron issues — main and renderer process setup, IPC communication with contextBridge, preload scripts, auto-update, native module rebuilding, and packaging with electron-builder.

The Problem

The Electron app starts but the window is blank:

const win = new BrowserWindow({ width: 800, height: 600 });
win.loadURL('http://localhost:3000');
// Window opens but shows a white screen

Or IPC messages from the renderer don’t reach the main process:

// renderer.js
const { ipcRenderer } = require('electron');
ipcRenderer.send('save-file', data);
// Error: require is not defined

Or the app works in development but the packaged build crashes:

Error: Cannot find module './main.js'

Or native modules fail after packaging:

Error: The module 'better-sqlite3' was compiled against a different Node.js version

Why This Happens

Electron runs two separate process types with different capabilities and security models:

  • Main process has full Node.js access — it creates windows, accesses the filesystem, and manages the app lifecycle. It runs main.js (or whatever you set in package.json’s main field).
  • Renderer process runs in a Chromium sandbox — each BrowserWindow is a separate renderer. For security, modern Electron disables Node.js integration in renderers by default (nodeIntegration: false, contextIsolation: true). Direct require('electron') or require('fs') in the renderer no longer works.
  • Communication uses IPC through a preload script — the preload script runs in the renderer but has access to a limited set of Electron APIs. It uses contextBridge to safely expose specific functions to the renderer’s window object. Skipping this pattern (enabling nodeIntegration) is a security risk.
  • Native modules must match Electron’s Node.js version — Electron bundles its own Node.js. Native addons compiled for system Node.js won’t work. They must be rebuilt with electron-rebuild for the correct Electron version and architecture.

Fix 1: Set Up Main Process and Window

npm install electron --save-dev
npm install electron-builder --save-dev  # For packaging
// src/main/main.ts — main process
import { app, BrowserWindow, ipcMain } from 'electron';
import path from 'path';

let mainWindow: BrowserWindow | null = null;

function createWindow() {
  mainWindow = new BrowserWindow({
    width: 1200,
    height: 800,
    webPreferences: {
      // Security: keep these defaults
      nodeIntegration: false,       // Don't expose Node.js in renderer
      contextIsolation: true,       // Isolate preload from renderer
      sandbox: true,                // Sandbox the renderer process
      preload: path.join(__dirname, 'preload.js'),  // Bridge script
    },
  });

  // Development: load from dev server
  if (process.env.NODE_ENV === 'development') {
    mainWindow.loadURL('http://localhost:5173');
    mainWindow.webContents.openDevTools();
  } else {
    // Production: load bundled HTML
    mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'));
  }

  mainWindow.on('closed', () => {
    mainWindow = null;
  });
}

// App lifecycle
app.whenReady().then(createWindow);

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

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

Fix 2: Secure IPC Communication

The preload script bridges main and renderer safely:

// src/main/preload.ts — runs in renderer context with limited Electron access
import { contextBridge, ipcRenderer } from 'electron';

// Expose specific APIs to the renderer via window.electronAPI
contextBridge.exposeInMainWorld('electronAPI', {
  // One-way: renderer → main
  saveFile: (content: string) =>
    ipcRenderer.send('save-file', content),

  // Two-way: renderer → main → renderer (returns a promise)
  openFile: () =>
    ipcRenderer.invoke('open-file'),

  readSettings: () =>
    ipcRenderer.invoke('read-settings'),

  writeSettings: (settings: Record<string, unknown>) =>
    ipcRenderer.invoke('write-settings', settings),

  // Main → renderer (listen for events)
  onUpdateAvailable: (callback: (version: string) => void) => {
    const handler = (_event: Electron.IpcRendererEvent, version: string) =>
      callback(version);
    ipcRenderer.on('update-available', handler);
    // Return cleanup function
    return () => ipcRenderer.removeListener('update-available', handler);
  },

  // Platform info
  platform: process.platform,
});
// src/main/main.ts — handle IPC in main process
import { ipcMain, dialog } from 'electron';
import fs from 'fs/promises';

// Handle invoke (two-way)
ipcMain.handle('open-file', async () => {
  const result = await dialog.showOpenDialog(mainWindow!, {
    properties: ['openFile'],
    filters: [
      { name: 'Text Files', extensions: ['txt', 'md', 'json'] },
      { name: 'All Files', extensions: ['*'] },
    ],
  });

  if (result.canceled || result.filePaths.length === 0) return null;

  const content = await fs.readFile(result.filePaths[0], 'utf-8');
  return { path: result.filePaths[0], content };
});

// Handle send (one-way)
ipcMain.on('save-file', async (event, content: string) => {
  const result = await dialog.showSaveDialog(mainWindow!, {
    filters: [{ name: 'Text', extensions: ['txt'] }],
  });

  if (!result.canceled && result.filePath) {
    await fs.writeFile(result.filePath, content, 'utf-8');
  }
});

// Settings storage
const settingsPath = path.join(app.getPath('userData'), 'settings.json');

ipcMain.handle('read-settings', async () => {
  try {
    const data = await fs.readFile(settingsPath, 'utf-8');
    return JSON.parse(data);
  } catch {
    return {};
  }
});

ipcMain.handle('write-settings', async (_event, settings) => {
  await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2));
});
// src/renderer/App.tsx — use the exposed API in React/Vue/Svelte
// TypeScript declaration for the exposed API
declare global {
  interface Window {
    electronAPI: {
      saveFile: (content: string) => void;
      openFile: () => Promise<{ path: string; content: string } | null>;
      readSettings: () => Promise<Record<string, unknown>>;
      writeSettings: (settings: Record<string, unknown>) => Promise<void>;
      onUpdateAvailable: (callback: (version: string) => void) => () => void;
      platform: string;
    };
  }
}

function App() {
  const [content, setContent] = useState('');

  async function handleOpen() {
    const file = await window.electronAPI.openFile();
    if (file) {
      setContent(file.content);
    }
  }

  function handleSave() {
    window.electronAPI.saveFile(content);
  }

  useEffect(() => {
    const cleanup = window.electronAPI.onUpdateAvailable((version) => {
      alert(`Update available: ${version}`);
    });
    return cleanup;
  }, []);

  return (
    <div>
      <button onClick={handleOpen}>Open File</button>
      <button onClick={handleSave}>Save File</button>
      <textarea value={content} onChange={e => setContent(e.target.value)} />
    </div>
  );
}

Fix 3: Integrate with Vite

Use electron-vite or vite-plugin-electron for a modern dev experience:

npm install -D electron-vite
// electron.vite.config.ts
import { defineConfig, externalizeDepsPlugin } from 'electron-vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  main: {
    plugins: [externalizeDepsPlugin()],
    build: {
      outDir: 'out/main',
    },
  },
  preload: {
    plugins: [externalizeDepsPlugin()],
    build: {
      outDir: 'out/preload',
    },
  },
  renderer: {
    plugins: [react()],
    build: {
      outDir: 'out/renderer',
    },
  },
});
// package.json
{
  "main": "out/main/main.js",
  "scripts": {
    "dev": "electron-vite dev",
    "build": "electron-vite build",
    "preview": "electron-vite preview"
  }
}

Fix 4: Native Module Rebuilding

Native modules (better-sqlite3, sharp, etc.) must match Electron’s Node.js:

# Install electron-rebuild
npm install -D @electron/rebuild

# Rebuild all native modules for current Electron version
npx @electron/rebuild

# Or rebuild a specific module
npx @electron/rebuild -m node_modules/better-sqlite3

# Add to postinstall for automatic rebuilding
// package.json
{
  "scripts": {
    "postinstall": "electron-builder install-app-deps"
  }
}

For better-sqlite3 specifically:

# If rebuild fails, install build tools
# Windows: npm install -g windows-build-tools
# macOS: xcode-select --install
# Linux: sudo apt install build-essential python3

npx @electron/rebuild -m node_modules/better-sqlite3

Fix 5: Package and Distribute

npm install -D electron-builder
// package.json — electron-builder config
{
  "build": {
    "appId": "com.example.myapp",
    "productName": "My App",
    "directories": {
      "output": "release"
    },
    "files": [
      "out/**/*",
      "package.json"
    ],
    "mac": {
      "target": ["dmg", "zip"],
      "icon": "build/icon.icns",
      "category": "public.app-category.developer-tools",
      "hardenedRuntime": true,
      "notarize": true
    },
    "win": {
      "target": ["nsis", "portable"],
      "icon": "build/icon.ico"
    },
    "linux": {
      "target": ["AppImage", "deb"],
      "icon": "build/icons",
      "category": "Development"
    },
    "nsis": {
      "oneClick": false,
      "allowToChangeInstallationDirectory": true
    }
  }
}
# Build for current platform
npx electron-builder

# Build for specific platform
npx electron-builder --mac
npx electron-builder --win
npx electron-builder --linux

# Build for all platforms (requires CI or cross-compilation)
npx electron-builder -mwl

Fix 6: Auto-Update

npm install electron-updater
// src/main/updater.ts
import { autoUpdater } from 'electron-updater';
import { BrowserWindow } from 'electron';

export function setupAutoUpdater(mainWindow: BrowserWindow) {
  // Check for updates on startup
  autoUpdater.checkForUpdatesAndNotify();

  // Check periodically (every 4 hours)
  setInterval(() => {
    autoUpdater.checkForUpdatesAndNotify();
  }, 4 * 60 * 60 * 1000);

  autoUpdater.on('update-available', (info) => {
    mainWindow.webContents.send('update-available', info.version);
  });

  autoUpdater.on('update-downloaded', (info) => {
    mainWindow.webContents.send('update-downloaded', info.version);
  });

  autoUpdater.on('error', (error) => {
    console.error('Auto-updater error:', error);
  });
}

// In main.ts
import { setupAutoUpdater } from './updater';

function createWindow() {
  const win = new BrowserWindow({ /* ... */ });
  // Set up auto-updater after window is ready
  if (process.env.NODE_ENV !== 'development') {
    setupAutoUpdater(win);
  }
}

// Trigger update install from renderer
ipcMain.on('install-update', () => {
  autoUpdater.quitAndInstall();
});
// package.json — publish config for GitHub Releases
{
  "build": {
    "publish": {
      "provider": "github",
      "owner": "your-username",
      "repo": "your-repo"
    }
  }
}

Still Not Working?

White screen when loading a fileloadFile path is relative to the build output. If your renderer HTML is at out/renderer/index.html, use path.join(__dirname, '../renderer/index.html'). In development, use loadURL('http://localhost:5173') instead. Check the DevTools console (mainWindow.webContents.openDevTools()) for 404 errors.

require is not defined in renderer — this is intentional. Electron disables nodeIntegration by default for security. Don’t enable it. Use the preload + contextBridge pattern shown in Fix 2. Every Node.js operation should go through IPC to the main process.

App is huge (200MB+) — Electron bundles Chromium, so a minimum of ~80MB is expected. To reduce size: use electron-builder’s asar packaging (enabled by default), exclude unnecessary files in the files config, and avoid bundling dev dependencies. For significantly smaller desktop apps, consider Tauri instead.

macOS notarization fails — Apple requires apps to be signed and notarized. Set mac.hardenedRuntime: true and configure your Apple Developer credentials. For CI, use electron-notarize or electron-builder’s built-in notarize config with APPLE_ID and APPLE_APP_SPECIFIC_PASSWORD environment variables.

For related desktop app issues, see Fix: Tauri Not Working and Fix: React useState Not Updating.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles