Skip to main content

Fixing 'node-gyp' Rebuild Errors with Better-SQLite3 in Electron

 If you are integrating better-sqlite3 into an Electron application, you have likely encountered the following stack trace upon starting your application:

Error: The module '\\?\C:\path\to\app\node_modules\better-sqlite3\build\Release\better_sqlite3.node'
was compiled against a different Node.js version using NODE_MODULE_VERSION 115. 
This version of Node.js requires NODE_MODULE_VERSION 121. 
Please try re-compiling or re-installing the module (for instance, using npm rebuild or npm install).

This is the quintessential native module ABI mismatch. It stops production builds and local development servers dead in their tracks. This post details why this occurs at a binary level and provides an automated, infrastructure-as-code solution to fix it permanently.

The Root Cause: ABI and V8 Divergence

Node.js allows developers to write add-ons in C++ (like better-sqlite3). When you run npm install, the node-gyp build tool compiles this C++ code into a binary file (.node) specifically for the runtime environment executing the installation process—typically the Node.js version installed on your host machine (e.g., Node 20).

However, Electron does not use the host's Node.js runtime. Electron embeds its own version of Node.js and, more importantly, a specific version of the V8 JavaScript engine.

Even if your local Node version and Electron's internal Node version share the same major version number, they often differ in their ABI (Application Binary Interface) signature. The ABI determines how data structures and function calls are mapped in memory. If the compiled binary expects the memory layout of Node 20 (Host) but is loaded into the memory layout of Electron (Runtime), the application will crash to prevent memory corruption.

The NODE_MODULE_VERSION is an integer representing this ABI generation. The error explicitly states that the binary was built for one generation (Host) but the runtime demands another (Electron).

The Fix: Automated Lifecycle Recompilation

To solve this, we must ensure that native modules are recompiled against the Electron headers immediately after installation and during the packaging pipeline. We will use @electron/rebuild, the official package designed to handle the complex arguments required to target Electron's headers.

Step 1: Install Dependencies

Do not use global CLI tools. We want the build process to be deterministic and contained within the repository.

npm install better-sqlite3
npm install --save-dev @electron/rebuild

Step 2: Configure The Post-Install Hook

The most robust way to handle this for local development is to hook into npm install. This ensures that whenever a developer (or CI pipeline) installs dependencies, the native modules are immediately rebuilt for the project's specific Electron version.

Modify your package.json:

{
  "scripts": {
    "start": "electron .",
    "postinstall": "electron-rebuild",
    "build": "electron-builder"
  }
}

Note: If you are using a monorepo or a complex setup where generic rebuilding is dangerous, you can scope the rebuild command to specifically target the database driver:

"postinstall": "electron-rebuild -f -w better-sqlite3"

Step 3: Configure Bundler Exclusion (Vite/Webpack)

This is the step most developers miss. While the ABI issue is fixed by recompilation, bundlers like Vite or Webpack will attempt to bundle the .node binary or the JavaScript entry point of better-sqlite3 incorrectly, causing runtime failures.

You must treat better-sqlite3 as an external dependency so that Electron loads the compiled binary from the node_modules folder at runtime (Node-style resolution) rather than bundling it.

For Vite (electron-vite / vite.config.ts):

import { defineConfig } from 'vite';

export default defineConfig({
  build: {
    rollupOptions: {
      external: [
        // Exclude the driver from the bundle
        'better-sqlite3'
      ]
    }
  }
});

For Webpack (webpack.config.js):

module.exports = {
  // ... configuration
  externals: {
    'better-sqlite3': 'commonjs better-sqlite3'
  }
};

Step 4: Configure Electron Builder

When packaging for production, electron-builder usually handles native modules automatically, but explicit configuration ensures that the rebuilding process targets the correct architecture (e.g., compiling for arm64 on an x64 machine).

Add this configuration to your package.json (or electron-builder.yml):

{
  "build": {
    "npmRebuild": true,
    "asar": true,
    "asarUnpack": [
      "**/node_modules/better-sqlite3/**"
    ]
  }
}
  • npmRebuild: Forces a check to ensure native deps are rebuilt before packaging.
  • asarUnpack: This is critical. better-sqlite3 loads a binary file. While Electron supports reading some files from within an ASAR archive, native binaries often fail or behave unpredictably when executed directly from a compressed archive. Unpacking it ensures the OS can load the .node shared library correctly.

The Explanation: Why This Works

  1. Header Downloading: When @electron/rebuild runs, it detects the version of electron installed in your devDependencies. It downloads the C++ headers for that specific Electron version from the Electron asset repository, rather than using the headers from your local Node installation.
  2. node-gyp Orchestration: It invokes node-gyp with the --dist-url flag pointing to those Electron headers and sets the --target flag to the Electron version.
  3. Lifecycle Automation: By placing this in postinstall, you eliminate "it works on my machine" issues. Any developer cloning the repo will automatically get the correct binaries after npm install.
  4. Externalization: By excluding the module from Vite/Webpack, we rely on the CommonJS module resolution built into Electron to find the correctly compiled binary in node_modules.

Conclusion

The NODE_MODULE_VERSION mismatch is a deterministic constraint of using native add-ons in Electron. It is not a bug, but a requirement of the V8 architecture. By automating the rebuild process via postinstall hooks and correctly externalizing the module in your bundler, you ensure that your SQLite database acts as a stable, high-performance foundation for your desktop application.