Fix: Electron 'require' Is Not Defined Error
Part of: JavaScript & TypeScript Errors
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.
The first instinct is almost always to set nodeIntegration: true and move on. That works, the error disappears, and the renderer can call require again — but you have just reverted a security default that the Electron team spent years rolling out. Any HTML you load (including a CDN link that goes stale and gets squatted, or any future XSS in your own code) now has the ability to read the user’s home directory, run arbitrary binaries, and exfiltrate data over the network. The correct fix is not to disable the protection; it is to use the preload + contextBridge pattern, which gives the renderer exactly the capabilities you choose to grant and nothing else.
The second confusing layer is that Electron’s defaults have shifted across versions. Electron 5 disabled nodeIntegration. Electron 12 enabled contextIsolation. Electron 14 removed the legacy remote module. Electron 20 enabled the OS-level sandbox by default. Tutorials written before each of these changes look correct but no longer work, and copy-pasting a webPreferences block from an old Stack Overflow answer is the easiest way to end up with a half-broken security posture. Always check the version against the Electron breaking-changes page before trusting a snippet.
Diagnostic Timeline
When the ReferenceError fires the first time, walk through this list instead of jumping to nodeIntegration: true.
Minute 0 — Confirm the version. Run npx electron --version. If you are on 12 or higher, contextIsolation is on and nodeIntegration is off by default. The error is expected, and the fix is architectural, not a one-line toggle.
Minute 2 — Verify the preload script is actually loading. Add console.log('preload loaded') at the top of preload.js. The line appears in the main process terminal, not the renderer DevTools. If you see nothing, the preload path is wrong or the file errored before reaching that line.
Minute 5 — Confirm the absolute path. Open the main process file and check that webPreferences.preload uses path.join(__dirname, 'preload.js') — never a bare string, never a relative path. Packaged apps run from app.asar and resolve ./preload.js to a different directory than development does.
Minute 8 — Test the bridge. Add contextBridge.exposeInMainWorld('debug', { ping: () => 'pong' }) to the preload, then in the renderer DevTools console type window.debug.ping(). If this returns 'pong', the preload is wired up correctly and the error is just that your code is trying to use require directly instead of going through the bridge.
Minute 12 — Audit the renderer for direct Node calls. Search the renderer source for require(. Every match is a place you need to either remove or replace with a call to window.electronAPI.something() that you defined in the preload.
Minute 15 — Check for TypeScript misconfiguration. If the preload is written in TypeScript and your tsconfig.json emits ESM ("module": "esnext"), the compiled preload will use import statements that Electron’s preload context cannot evaluate. The file loads, errors silently, and the renderer sees no window.electronAPI. Compile the preload with "module": "commonjs".
Minute 18 — Rule out webview and BrowserView. A <webview> tag has its own preload attribute, completely independent of the parent window’s webPreferences.preload. Same for BrowserView. If the error is happening inside one of those, the parent’s preload does not apply.
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.
Inspect what the renderer actually sees. Type
Object.keys(window)in the renderer DevTools. If the keys you exposed viacontextBridge.exposeInMainWorldare not present, the preload either errored out, was never registered, or registered against the wrong window. Confirm by adding a deliberate throw inside the preload and checking the main process logs.Beware bundler externals when shipping a preload through webpack. A webpack-bundled preload that imports
electronas a regular dependency tries to resolve it through node_modules at runtime and fails. Markelectronas an external (externals: { electron: 'commonjs electron' }) so the preload uses Electron’s built-in module.Check for Electron Forge or electron-builder packaging mistakes. A common mistake is excluding the preload file from the packaged build because it lives outside the
src/directory listed in your packager config. Open the generatedapp.asar(npx asar list app.asar) and verifypreload.jsis present.Rule out unhandled rejections inside the preload. A rejected promise at the top level of the preload script aborts the load on some Electron versions without printing a clear error. Wrap top-level awaits in try/catch and log the error explicitly.
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: Kafka Consumer Not Receiving Messages, Connection Refused, and Rebalancing Errors
How to fix Apache Kafka issues — consumer not receiving messages, auto.offset.reset, Docker advertised.listeners, max.poll.interval.ms rebalancing, MessageSizeTooLargeException, and KafkaJS errors.
Fix: OpenAI API Not Working — RateLimitError, 401, 429, and Connection Issues
How to fix OpenAI API errors — RateLimitError (429), AuthenticationError (401), APIConnectionError, context length exceeded, model not found, and SDK v0-to-v1 migration mistakes.