Call destroy() on native addon before app quit
You must call destroy() on a native addon before the Electron app quits, ideally in the will-quit or before-quit event handler. Without this, persistent references to callbacks and the threadsafe function will prevent the native addon's destructor from running, causing Electron to hang on quit.
JavaScript wrapper for C++ native addon with EventEmitter
A wrapper class should extend EventEmitter to handle native events. It should load the addon using require('bindings') only on the target platform (e.g., Linux), forward events from C++ to JavaScript using this.emit(), provide clean methods to call into C++, include a destroy() method to release native resources, and parse JSON data into proper JavaScript objects including date conversion.
Platform-specific native addon loading pattern
Check process.platform at module load time to conditionally load the native addon. Throw an error if the module is loaded on an unsupported platform. Export an empty object {} on non-supported platforms so the module loads without error but provides no functionality.
Event forwarding from C++ addon to JavaScript
Listen to events from the C++ addon instance using addon.on(), then re-emit those events using this.emit() in the JavaScript wrapper. This allows JavaScript code to subscribe to native events with standard JavaScript event listeners.
Native addon JSON data parsing example
When receiving JSON payloads from C++, parse them with JSON.parse() and then convert special fields like dates from strings to JavaScript Date objects using new Date(). This example shows: const parsed = JSON.parse(payload); return { ...parsed, date: new Date(parsed.date) }
Building native Node.js addons
After configuring binding.gyp and writing C++/C code, run npm run build to compile the native addon. Once built successfully, the addon can be imported or required in the Electron application.
GTK3 compatible with Electron for native Linux UIs
GTK3 is compatible with Electron's Chromium runtime and can be used in native addons to create native Linux user interfaces. GTK3 GUI operations can run in their own thread and communicate back to JavaScript through events.
Complete C++ addon usage example in Electron
Example showing how to use a compiled C++ addon in an Electron process: import cppLinux from 'cpp-linux'; console.log(cppLinux.helloWorld('Hi!')); // Output: "Hello from C++! You said: Hi!"; cppLinux.on('todoAdded', (todo) => { console.log('New todo added:', todo); }); cppLinux.on('todoUpdated', (todo) => { console.log('Todo updated:', todo); }); cppLinux.on('todoDeleted', (todo) => { console.log('Todo deleted:', todo); }); cppLinux.helloGui();
Native addon todo application example
A complete C++ addon can implement a GTK3 GUI with: a text entry field for todo items, a calendar widget for selecting dates, an Add button to create new todos, a scrollable list showing all todos, and right-click context menus for editing and deleting todos. User interactions trigger JavaScript events allowing real-time response.
Phase Three success criteria
Phase Three is complete when `node script/node-spec-runner.js --default` exits with zero failures and all changes are committed per the commit guidelines. Do not stop until these criteria are met.
When to edit patches
During active `git am` conflict: fix in node repo, then run `git am --continue`. Modifying patch outside conflict: edit `.patch` file directly. Creating new patch (rare, avoid): commit in node repo, then run `e patches node`. Fix existing patches 99% of the time rather than creating new ones.
Node.js upgrade workflow has three phases
Phase One: Run `e sync --3` repeatedly, fixing patch conflicts as they arise, until it succeeds, then export patches and commit changes. Phase Two: Run `e build -k 999 -- --quiet` repeatedly, fixing build issues, until it succeeds, then run `e start --version` to validate Electron launches, and commit changes. Phase Three: Run `node script/node-spec-runner.js --default`, fix failing tests, and commit fixes until all tests pass.
Patch system mental model
Patches flow: `patches/node/*.patch` → `[e sync --3]` → `../third_party/electron_node` commits. Commits flow back: `../third_party/electron_node` commits → `[e patches]` → `patches/node/*.patch`.
Do not delete patches unless 100% certain they are no longer needed
Never delete or skip patches without verification. For major version upgrades, patches that shim deprecated V8 APIs or backport upstream changes are often deletable because the new Node.js version already incorporates them—but verify before removing. Complicated conflicts or hard to resolve issues should be presented to the user after exhausting all other options. Do not delete a patch just because you cannot solve it.
Never use git am --skip to recreate patches
Never use `git am --skip` and then manually recreate a patch by making a new commit. This destroys the original patch's authorship, commit message, and position in the series. If `git am --continue` reports 'No changes', investigate why—the changes were likely absorbed by a prior conflict resolution's 3-way merge. Present this situation to the user rather than skipping and recreating.
Clear rerere cache before starting an upgrade session
Run `git rerere clear` in both the electron and `../third_party/electron_node` repos at the start of each upgrade session. Stale recorded resolutions from a prior attempt can silently apply wrong merges.
Ensure pre-commit hooks are installed before upgrade
Check that `.git/hooks/pre-commit` exists in the electron repo. If not, run `yarn husky` to install it. The hook runs `lint-staged` which handles clang-format for C++ files.
Phase One workflow steps
1. Run `e sync --3` (the `--3` flag enables 3-way merge, always required). 2. If succeeds, skip to step 5. 3. If patch fails: identify target repo and patch from error output, analyze failure, fix conflict in `../third_party/electron_node` working directory, run `git am --continue` in `../third_party/electron_node`, repeat until all patches for that repo apply. IMPORTANT: Once `git am --continue` succeeds you MUST run `e patches node` to export fixes. Return to step 1. 4. When `e sync --3` succeeds, run `e patches all`. 5. Read `references/phase-one-commit-guidelines.md` NOW, then commit changes following those instructions exactly.
Phase One success criteria
Phase One is complete when `e sync --3` exits with code 0 (no patch failures) and all changes are committed per the commit guidelines. Do not stop until these criteria are met.
Phase Two workflow steps
1. Run `e build -k 999 -- --quiet` (the `--quiet` flag suppresses per-target status lines, showing only errors and the final result). 2. If succeeds, skip to step 6. 3. If build fails: identify underlying file in 'electron' from the compilation error message, analyze failure, fix build issue by adapting Electron's code for the change in Node.js, run `e build -t {target_that_failed}.o` to build just the failed target. The target name can be identified from the failure line in the build log. 4. Read `references/phase-two-commit-guidelines.md` NOW, then commit changes following those instructions exactly. 5. After ANY commit (especially patch commits), immediately run `git status` in the electron repo. Look for other modified `.patch` files that only have index/hunk header changes. These are dependent patches affected by your fix. Commit them immediately with: `git commit -am "chore: update patches (trivial only)"`. Return to step 1. 6. When `e build` succeeds, run `e start --version`. 7. Check if you have any pending changes in the Node.js repo by running `git status` in `../third_party/electron_node`. If you have changes follow the instructions for patch fixes.
Phase Two success criteria
Phase Two is complete when `e build -k 999 -- --quiet` exits with code 0 (no build failures), `e start --version` has been run to check Electron launches, and all changes are committed per the commit guidelines. Do not stop until these criteria are met. Do not delete code or features, never comment out code in order to take short cut. Make all existing code, logic and intention work.
Phase Two patch fixes workflow
When the error is in a file that Electron patches (check with `grep -l "filename" patches/node/*.patch`): 1. Edit the file in the Node.js source tree (`../third_party/electron_node/...`). 2. Create a fixup commit targeting the original patch commit: `cd ../third_party/electron_node`, `git add <modified-file>`, `git commit --fixup=<original-patch-commit-hash>`, `GIT_SEQUENCE_EDITOR=: git rebase --autosquash --autostash -i <commit>^`. 3. Export the updated patch: `e patches node`. 4. Commit the updated patch file following `references/phase-one-commit-guidelines.md`. To find the original patch commit to fixup: `git log --oneline | grep -i "keyword from patch name"`. The base commit for rebase is the Node.js commit before patches were applied. Find it by checking the `refs/patches/upstream-head` ref.
Phase Two Electron code fixes
When the error is in Electron's own source code (files in shell/, electron/, etc.): 1. Edit files directly in the electron repo. 2. Commit directly (no patch export needed).
Phase Three workflow steps
1. Run `node script/node-spec-runner.js --default` from the electron repo. 2. If all tests pass, Phase Three is complete. 3. If tests fail: identify the failing test file(s) from the output, analyze each failure, fix the test in `../third_party/electron_node/test/...`, re-run the specific failing test to verify: `node script/node-spec-runner.js {test-path}`. The test path is relative to the node `test/` directory, e.g. `test/parallel/test-crypto-key-objects-raw.js`. Do NOT use `--default` when running specific tests. Do NOT run tests directly with `ELECTRON_RUN_AS_NODE`—the runner handles environment setup. Commit the fix using the fixup workflow and commit guidelines. Return to step 1.
BoringSSL incompatibilities in Node.js test suite
Electron builds Node.js against Chromium's BoringSSL instead of Node.js's bundled OpenSSL. Upstream Node.js now supports building against BoringSSL natively (enabled via `node_openssl_path = "//third_party/boringssl"` in `build/args/all.gn`), so most of Electron's historical BoringSSL workarounds have been eliminated. Expect to delete these workarounds over time rather than grow them. Only add a guard when a feature is genuinely still missing from BoringSSL; upstream tests increasingly self-skip when `process.features.openssl_is_boringssl` is set, so no Electron change is needed.
Preferred guard for BoringSSL-unsupported features
Skip the whole test file at the top and add it to `fix_crypto_tests_to_run_with_bssl.patch` using this pattern: `if (process.features.openssl_is_boringssl) { common.skip('Skipping unsupported feature tests'); }`. For tests that cannot be cleanly guarded inline, add the whole file to `script/node-disabled-tests.json` instead.
BoringSSL unsupported features as of v24.18.0
The following features are still unsupported in Chromium's BoringSSL as of v24.18.0: RSA-PSS keygen (deprecation path, file-level `common.skip` in `test-crypto-keygen-deprecation`), ML-DSA keys (file-level `common.skip` in `test-crypto-pqc-key-objects-ml-dsa`), ML-KEM keys (disabled in `node-disabled-tests.json` in `test-crypto-pqc-key-objects-ml-kem`), FIPS mode (disabled in `test-crypto-fips`), Secure heap (disabled in `test-crypto-secure-heap`), Stateless DH (disabled in `test-crypto-dh-stateless`), Assorted keygen / WebCrypto keygen (disabled in `test-crypto-keygen`, `test-webcrypto-keygen`, `wpt/test-webcrypto`).
BoringSSL behavioral differences from OpenSSL
Some errors in BoringSSL just changed shape compared to OpenSSL and require assertion updates rather than test skips. For example, creating a private key from an unsupported OKP (Ed448) JWK now throws `Invalid JWK OKP key` (previously `Invalid JWK data`); see nodejs/node#62499. Electron's `spec/node-spec.ts` assertion was loosened to `/Invalid JWK/`. When guarding a test, prefer a precise capability check (e.g. `ciphers.includes('aes-128-ccm')`) over a blanket `process.features.openssl_is_boringssl` check where the feature can be probed directly.
Snapshot test regeneration for V8 differences
Some tests compare output against committed `.snapshot` files using `assert.strictEqual`—these are NOT wildcard comparisons. When Chromium's V8 produces different output (e.g. different stack traces due to V8 enhancements), the snapshot must be regenerated: `NODE_REGENERATE_SNAPSHOTS=1 node script/node-spec-runner.js test/test-runner/test-foo.mjs`. Then inspect the diff to verify the changes are expected, and commit the updated snapshot into the appropriate patch.
Phase Three patch fixes workflow
Most test fixes go into existing patches in `patches/node/`. Use the fixup workflow: 1. Edit the test file in `../third_party/electron_node/test/...`. 2. Find the relevant patch commit: `git log --oneline | grep -i "keyword"`. Crypto/BoringSSL tests → `fix crypto tests to run with bssl`. Snapshot tests → the specific snapshot patch (e.g. `test: accomodate V8 thenable`). Flaky tests → `test: formally mark some tests as flaky`. 3. Create a fixup commit: `cd ../third_party/electron_node`, `git add test/path/to/test.js`, `git commit --fixup=<patch-commit-hash>`, `GIT_SEQUENCE_EDITOR=: git rebase --autosquash --autostash -i <commit>^`. 4. Export: `e patches node`. 5. Read `references/phase-three-commit-guidelines.md` NOW, then commit the updated patch file.
When to add tests to disabled tests list
Only add a test to `script/node-disabled-tests.json` as a last resort—when the test is fundamentally incompatible with Electron's architecture (not just a BoringSSL difference that can be guarded). Tests disabled here are completely skipped and never run.
High-churn patches requiring work during Node.js upgrades
`fix_handle_boringssl_and_openssl_incompatibilities.patch` — Electron uses BoringSSL (via Chromium) while Node.js expects OpenSSL. Historically large and complex, this patch was greatly reduced once Node.js gained native BoringSSL support. It still shims some C++-level differences, and upstream OpenSSL/ncrypto API changes can break it. `fix_crypto_tests_to_run_with_bssl.patch` — Companion to the above; adapts Node.js crypto tests for BoringSSL. Also greatly reduced now that upstream tests self-skip under BoringSSL. `support_v8_sandboxed_pointers.patch` — V8 sandbox pointer support requires careful adaptation when V8 APIs change. `build_add_gn_build_files.patch` — The GN build file patch is large and touches many build targets. Upstream build system changes frequently conflict.
Major Node.js version upgrade expectations
Major Node.js version transitions (e.g., v22 → v24) are significantly more involved than patch bumps. Expect patch deletions—Electron uses Chromium's V8, which is often ahead of the V8 version bundled in Node.js. Many patches exist to bridge this gap. When Node.js bumps to a newer major version, its V8 catches up to Chromium's, and those bridge patches can be deleted. In the v22 → v24 upgrade, 17 patches were deleted for this reason. Update `@types/node` in `package.json` to match the new major version. Post-upgrade regressions are expected. Even after the upgrade lands, follow-up fix PRs for edge cases (ESM path handling, certificate loading, platform-specific issues) are normal.
Node.js version bump versus major upgrade types
There are two types of Node.js version updates. Bumps (patch/minor) are automated by `electron-roller[bot]` with commit title `chore: bump node to v{version}`. Trivial patch index updates are handled automatically by `patchup[bot]`. These often land cleanly but may require manual patch fixes. Major upgrades (e.g., v22 → v24) are manual, large PRs with commit title `chore: upgrade Node.js to v{X}.{Y}.{Z}`. These typically involve deleting obsolete patches, adapting many others, and updating `@types/node` in `package.json`.
Key directories for Node.js upgrade work
Current directory: Electron repo (always run `e` commands here). `../third_party/electron_node`: Node.js repo (where patches apply). `patches/node/`: Patch files for Node.js. `docs/development/patches.md`: Patch system documentation.
Patch fixing rules
1. Preserve authorship: Keep original author in TODO comments (from patch `From:` field). 2. Never change TODO assignees: `TODO(name)` must retain original name. 3. Update descriptions: If upstream changed APIs or macros, update patch commit message to reflect current state. 4. Never skip-and-recreate a patch: If `git am --continue` says 'No changes — did you forget to use git add?', do NOT run `git am --skip` and create a replacement commit.
Commands for Node.js upgrade workflow
`e sync --3` — Clone deps and apply patches with 3-way merge. `git am --continue` — Continue after resolving conflict (run in node repo). `e patches node` — Export commits from node repo to patch files. `e patches all` — Export all patches from all targets. `e patches node --commit-updates` — Export patches and auto-commit trivial changes. `e patches --list-targets` — List targets and config paths. `e build -k 999 -- --quiet` — Build Electron, continue on errors, suppress status lines. `e build -t {target}.o` — Build just one specific target to verify a fix. `e start --version` — Validate Electron launches after successful build. `node script/node-spec-runner.js --default` — Run full Node.js test suite. `node script/node-spec-runner.js test/parallel/test-foo.js` — Run a single test. `NODE_REGENERATE_SNAPSHOTS=1 node script/node-spec-runner.js test/test-runner/test-foo.mjs` — Regenerate snapshot for a snapshot-based test.
Key directories for Phase Two and Phase Three work
Current directory: Electron repo (always run `e` commands here). `../third_party/electron_node`: Node.js repo (do not touch this code to fix build issues, just read it to obtain context). `script/node-spec-runner.js` — Test runner script. `script/node-disabled-tests.json` — Permanently disabled tests (do not try to fix these). `../third_party/electron_node/test/` — Node.js test files (where patches apply). `patches/node/fix_crypto_tests_to_run_with_bssl.patch` — BoringSSL crypto test adaptations. `patches/node/test_formally_mark_some_tests_as_flaky.patch` — Flaky test list.
LanguageModelCloneOptions signal parameter
LanguageModelCloneOptions accepts a signal parameter of type AbortSignal, which allows cancellation of language model clone operations.