Fix: Electron 'require' Is Not Defined Error
Quick Answer
Fix the Electron 'require is not defined' error caused by contextIsolation, nodeIntegration changes, and learn to use preload scripts and contextBridge.
The Error
You open your Electron app and the renderer process throws:
Uncaught ReferenceError: require is not definedOr when trying to use Node.js modules in the browser window:
const fs = require('fs'); // ReferenceError: require is not defined
const { ipcRenderer } = require('electron'); // Same errorThis worked in older Electron versions but fails in Electron 12+.
Why This Happens
Starting with Electron 12, contextIsolation is enabled and nodeIntegration is disabled by default. This means the renderer process (your browser window) runs like a regular web page — it doesn’t have access to Node.js APIs like require, fs, path, or process.
This was a security change. Previously, any JavaScript running in the renderer (including third-party scripts, ads, or injected code) had full access to the filesystem and operating system through Node.js. This was a major security risk.
The solution is to use preload scripts and the contextBridge API to selectively expose only the Node.js functionality your renderer needs.
Fix 1: Use a Preload Script with contextBridge
This is the recommended approach. Create a preload script that exposes specific APIs to the renderer:
// preload.js
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
readFile: (path) => ipcRenderer.invoke('read-file', path),
writeFile: (path, data) => ipcRenderer.invoke('write-file', path, data),
onUpdateAvailable: (callback) => ipcRenderer.on('update-available', callback),
});Register it in your BrowserWindow:
// main.js
const { app, BrowserWindow, ipcMain } = require('electron');
const fs = require('fs');
const path = require('path');
function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true, // Default in Electron 12+
nodeIntegration: false, // Default in Electron 12+
},
});
win.loadFile('index.html');
}
// Handle IPC calls from the renderer
ipcMain.handle('read-file', async (event, filePath) => {
return fs.readFileSync(filePath, 'utf-8');
});
ipcMain.handle('write-file', async (event, filePath, data) => {
fs.writeFileSync(filePath, data);
});In your renderer:
// renderer.js (loaded by index.html)
const content = await window.electronAPI.readFile('/path/to/file');
console.log(content);Pro Tip: Never expose entire Node.js modules through contextBridge. Expose only the specific functions your renderer needs. This follows the principle of least privilege — if your renderer is compromised, the attacker only has access to the functions you explicitly exposed.
Fix 2: Understand the Security Model
Before using any workaround, understand why the defaults changed:
| Setting | Old Default | New Default (12+) | Purpose |
|---|---|---|---|
nodeIntegration | true | false | Prevents Node.js access in renderer |
contextIsolation | false | true | Isolates preload from renderer |
sandbox | false | true (20+) | OS-level process sandboxing |
With contextIsolation: true, the preload script runs in a separate JavaScript context from the renderer. This means:
- The renderer can’t access
requireor any Node.js APIs - The renderer can’t modify the preload script’s globals
- The preload script uses
contextBridgeto create a controlled API
This protects your app from XSS attacks. If an attacker injects JavaScript into your renderer, they can only call the functions you exposed through contextBridge, not arbitrary Node.js code.
Fix 3: Use IPC for Main-Renderer Communication
The Inter-Process Communication (IPC) pattern replaces direct Node.js usage in the renderer:
// main.js — handle requests from renderer
const { ipcMain, dialog } = require('electron');
ipcMain.handle('open-file-dialog', async () => {
const result = await dialog.showOpenDialog({
properties: ['openFile'],
filters: [{ name: 'Text', extensions: ['txt', 'md'] }],
});
return result.filePaths;
});
ipcMain.handle('get-app-version', () => {
return app.getVersion();
});// preload.js — bridge between main and renderer
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
openFileDialog: () => ipcRenderer.invoke('open-file-dialog'),
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
});// renderer.js
document.getElementById('open-btn').addEventListener('click', async () => {
const files = await window.electronAPI.openFileDialog();
console.log('Selected:', files);
});Use ipcMain.handle / ipcRenderer.invoke for request-response patterns. Use ipcMain.on / ipcRenderer.send for fire-and-forget messages.
Fix 4: Enable nodeIntegration (Not Recommended)
If you’re building an internal tool or prototype and security isn’t a concern, you can re-enable the old behavior:
const win = new BrowserWindow({
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
},
});This restores require in the renderer, but:
- Any loaded URL can execute Node.js code, including remote content
- XSS vulnerabilities become Remote Code Execution vulnerabilities
- This approach is explicitly discouraged by the Electron security guidelines
Only use this for:
- Quick prototypes you’ll rewrite
- Internal tools that never load external content
- Legacy apps during migration to the preload pattern
Common Mistake: Enabling
nodeIntegrationwithout disablingcontextIsolation(Electron 12+ default) still won’t give yourequirein the renderer. You need both:nodeIntegration: trueANDcontextIsolation: false.
Fix 5: Fix Preload Script Path Issues
The preload script must be specified as an absolute path:
// Wrong - relative path may not resolve correctly
webPreferences: {
preload: './preload.js'
}
// Wrong - __dirname might not be what you expect in packaged app
webPreferences: {
preload: __dirname + '/preload.js'
}
// Correct - use path.join for cross-platform compatibility
const path = require('path');
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}For packaged apps using asar archives, the preload path must account for the asar structure:
// In development
preload: path.join(__dirname, 'preload.js')
// In production (if using app.asar)
preload: path.join(app.getAppPath(), 'preload.js')Verify your preload script is being loaded by adding a console log:
// preload.js
console.log('Preload script loaded!');Check the main process console — preload logs appear there, not in the renderer’s DevTools.
Fix 6: Handle ES Modules in Electron
If your renderer uses ES modules (type="module" in package.json or <script type="module">), the import syntax differs:
<!-- index.html -->
<script type="module">
// Can't use require() here even with nodeIntegration
// ES modules in the renderer use the window API from contextBridge
const version = await window.electronAPI.getAppVersion();
</script>For the preload script, it always uses CommonJS (require) regardless of your package.json settings. The preload runs in a special Node.js context:
// preload.js — always CommonJS
const { contextBridge } = require('electron');
// import { contextBridge } from 'electron'; // This won't work in preloadIf your main process uses ESM ("type": "module"):
// main.mjs
import { app, BrowserWindow } from 'electron';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));Fix 7: Migrate from remote Module
The remote module was removed in Electron 14. If your code uses it:
// Old way (removed)
const { dialog } = require('electron').remote;
dialog.showOpenDialog(options);Migrate to IPC:
// main.js
ipcMain.handle('show-dialog', async (event, options) => {
return dialog.showOpenDialog(options);
});
// preload.js
contextBridge.exposeInMainWorld('electronAPI', {
showDialog: (options) => ipcRenderer.invoke('show-dialog', options),
});
// renderer.js
const result = await window.electronAPI.showDialog({
properties: ['openFile'],
});If you need remote temporarily during migration, install the community package:
npm install @electron/remote// main.js
require('@electron/remote/main').initialize();
require('@electron/remote/main').enable(win.webContents);This is a stopgap. Plan to migrate all remote usage to IPC.
Fix 8: Handle Electron Version-Specific Breaking Changes
Key version changes that affect require:
Electron 5: nodeIntegration defaults to false Electron 12: contextIsolation defaults to true Electron 14: remote module removed Electron 20: sandbox defaults to true
Check your Electron version:
npx electron --versionIf upgrading across multiple major versions, address each change:
// Electron 20+ compatible configuration
const win = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
// These are all defaults, listed for clarity:
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
},
});With sandbox: true (Electron 20+), even the preload script has limited Node.js access. You can use require for Electron modules (electron) but not arbitrary Node.js modules. Move all Node.js work to the main process and communicate via IPC.
Still Not Working?
Check for multiple BrowserWindows. Each window needs its own preload configuration. A preload script set on one window doesn’t apply to others.
Verify the preload script compiles. Syntax errors in the preload script fail silently. Test it with
node preload.jsto check for errors.Check TypeScript compilation. If using TypeScript, ensure the preload script is compiled to CommonJS, not ESM. Set
"module": "commonjs"in your tsconfig for the preload.Look for webview tags.
<webview>elements have their own preload attribute. The main window’s preload doesn’t apply to webviews.Clear the application cache. Old cached versions of your app may still use outdated preload scripts. Clear
userDatadirectory during development.Test in a clean environment. Create a minimal Electron app with just a main process, preload, and renderer to isolate the issue from your application code.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Firebase Permission Denied Error
How to fix the Firebase 'permission denied' or 'Missing or insufficient permissions' error in Firestore and Realtime Database. Covers security rules, authentication state, custom claims, Admin SDK, rule simulators, time-based rules, and document-level permissions.
Fix: React Warning: Failed prop type
How to fix the React 'Warning: Failed prop type' error. Covers wrong prop types, missing required props, children type issues, shape and oneOf PropTypes, migrating to TypeScript, default props, and third-party component mismatches.
Fix: Express Cannot GET /route (404 Not Found)
How to fix Express.js Cannot GET route 404 error caused by wrong route paths, missing middleware, route order issues, static files, and router mounting problems.
Fix: FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
How to fix the JavaScript heap out of memory error by increasing Node.js memory limits, fixing memory leaks, and optimizing builds in webpack, Vite, and Docker.