Fix: Electron Not Working — Window Not Showing, IPC Not Communicating, or Build Failing
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:
- 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 inpackage.json’smainfield). - Renderer process runs in a Chromium sandbox — each
BrowserWindowis a separate renderer. For security, modern Electron disables Node.js integration in renderers by default (nodeIntegration: false,contextIsolation: true). Directrequire('electron')orrequire('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
contextBridgeto safely expose specific functions to the renderer’swindowobject. Skipping this pattern (enablingnodeIntegration) 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-rebuildfor 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-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.
For related desktop app issues, see Fix: Tauri Not Working and Fix: React useState Not Updating.
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.