Skip to content

Fix: Electron 'require' Is Not Defined Error

FixDevs ·

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 defined

Or 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 error

This 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:

SettingOld DefaultNew Default (12+)Purpose
nodeIntegrationtruefalsePrevents Node.js access in renderer
contextIsolationfalsetrueIsolates preload from renderer
sandboxfalsetrue (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 require or any Node.js APIs
  • The renderer can’t modify the preload script’s globals
  • The preload script uses contextBridge to 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.

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 nodeIntegration without disabling contextIsolation (Electron 12+ default) still won’t give you require in the renderer. You need both: nodeIntegration: true AND contextIsolation: 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 preload

If 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 --version

If 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.js to 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 userData directory 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.

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