Fix: Electron Not Working — Window Not Showing, IPC Not Communicating, or Build Failing
Part of: JavaScript & TypeScript Errors
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 screenOr 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 definedOr 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 versionWhy This Happens
Electron runs two separate process types with different capabilities and security models. The 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). The 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 between the two layers 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 that disqualifies your app from most enterprise audits. Native modules must match Electron’s Node.js version exactly. Electron bundles its own Node.js, so native addons compiled for system Node.js won’t work. They must be rebuilt with electron-rebuild for the correct Electron version, ABI, and CPU architecture.
The blank-window failure is the most common symptom because so many things converge on the window-creation path: the preload script path may be wrong, the renderer URL may be unreachable, the content security policy may be blocking inline scripts, or the dev server may not be running. Each of those failures looks identical from the outside — a white rectangle — which is why you must open the DevTools console first before guessing.
Platform and Environment Differences
The same Electron code behaves very differently on the three target operating systems. On macOS, you must code-sign with a Developer ID Application certificate and submit the app to Apple’s notarization service. Notarization requires hardenedRuntime: true and a entitlements.plist that lists every Electron-specific entitlement you use (allow-jit, allow-unsigned-executable-memory, etc.). Apps shipped without notarization show the “developer cannot be verified” Gatekeeper dialog on macOS 10.15+. Apple Silicon (arm64) and Intel (x64) need separate builds or a universal binary built with --universal; native modules must be compiled for the matching arch or the app crashes on launch.
On Windows, code signing uses an EV (Extended Validation) certificate stored on a hardware token. Without an EV cert, SmartScreen flags downloads for the first few hundred installations until reputation builds. Cheaper OV certificates work but trigger more warnings. The default installer is NSIS, but electron-builder also produces MSI, AppX (Microsoft Store), and portable .exe outputs — each has slightly different update semantics. Windows path handling is case-insensitive but case-preserving, which means require('./Foo') works in dev and breaks on case-sensitive Linux CI builds.
On Linux, you choose between AppImage (single-file, runs anywhere), snap (Ubuntu’s sandboxed store), Flatpak (Fedora’s sandboxed runtime), or raw .deb/.rpm packages. Snap and Flatpak sandboxes block direct filesystem access outside ~/snap/<app>/ or ~/.var/app/<app>/, which breaks naive app.getPath('home') assumptions. AppImage avoids sandboxing but requires the host glibc to be recent enough for the bundled Chromium. Native modules need to be rebuilt against the target glibc — Alpine’s musl libc is not supported by upstream Electron at all.
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-sqlite3Fix 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 -mwlFix 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 file — loadFile 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.
App runs in dev but the packaged build crashes on launch with “module did not self-register” — a native module was built against a different ABI than the bundled Electron runtime. Pin electron to a specific version in package.json, run npx @electron/rebuild --force --arch=x64,arm64 (for macOS universal), and verify with process.versions.modules inside main that the module ABI matches. On Windows, also confirm that the build host has Visual Studio Build Tools 2022 installed — older versions silently produce binaries that fail to load on newer Windows 11 builds.
Auto-updater silently does nothing on macOS — macOS rejects updates from unsigned or improperly signed bundles. Even after notarization, the update must be packaged with the same Team ID as the installed app. Check ~/Library/Logs/<AppName>/main.log for Could not get code signature for running application — that means the running app and the new bundle have mismatched signatures.
Linux snap or Flatpak build can’t read user files — the sandbox blocks paths outside ~/snap/<app>/common/ (snap) or ~/.var/app/<app>/ (Flatpak). Either request the home interface in snapcraft.yaml / the --filesystem=home flag in your Flatpak manifest, or move user data to app.getPath('userData') which the sandbox always allows.
For related desktop app issues, see Fix: Tauri Not Working, Fix: Tauri 2 Not Working, Fix: Electron Forge Not Working, and Fix: Electron require is not defined.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Deno PermissionDenied — Missing --allow-read, --allow-net, and Other Flags
How to fix Deno PermissionDenied (NotCapable in Deno 2) errors — the right permission flags, path-scoped permissions, deno.json permission sets, and the Deno.permissions API.
Fix: Fastify Not Working — 404, Plugin Encapsulation, and Schema Validation Errors
How to fix Fastify issues — route 404 from plugin encapsulation, reply already sent, FST_ERR_VALIDATION, request.body undefined, @fastify/cors, hooks not running, and TypeScript type inference.
Fix: Vinxi Not Working — Dev Server Not Starting, Routes Not Matching, or Build Failing
How to fix Vinxi server framework issues — app configuration, routers, server functions, middleware, static assets, and deployment to different platforms.
Fix: Clack Not Working — Prompts Not Displaying, Spinners Stuck, or Cancel Not Handled
How to fix @clack/prompts issues — interactive CLI prompts, spinners, multi-select, confirm dialogs, grouped tasks, cancellation handling, and building CLI tools with beautiful output.