Technology
Node.js 25: what’s actually new, what broke in the ecosystem, and whether you should upgrade
A real-world review of Node.js 25 (Current): V8 14.1 and performance, Web Storage enabled by default (breaking tests/tools), the permission model with --allow-net, portable module compile cache (now stable), require(esm) marked stable, http.setGlobalProxyFromEnv(), fs.watch got ignore, and SEA can be built in one step with node --build-sea. Developer perspectives with links + a clear verdict: upgrade now or wait for LTS.

Prologue: you upgrade Node — and suddenly “localStorage” breaks your build
There are two kinds of Node upgrades. The first is “bump the version, everything works.” The second is “bump the version, and now your backend throws browser-looking errors.” Node.js 25 made a lot of teams experience the second kind.
Docusaurus looked surreal: DOMException [SecurityError]: Cannot initialize local storage without a --localstorage-file path during docusaurus build.[1] Similar symptoms showed up in the CLI world (Shopify) and in test runners (Jest/Vitest).[2][3][4]
This matters: it’s not just “a random bug in your repo.” It’s a consequence of Node 25 enabling Web Storage by default, while Node’s localStorage is not a 1:1 browser storage (file-backed, quota, shared behavior, no encryption).[7][5]
And still: it would be a mistake to reduce Node 25 to the Web Storage saga. 25.4.0 and 25.5.0 shipped genuinely mature improvements (require(esm) stable, compile cache stable, --build-sea) that reduce migration pain and improve DX — if you adopt them deliberately.[9][10][12]
A real case: docusaurus build fails on Node 25.x due to a localStorage SecurityError.[1]
A tiny Node 25 timeline: why 25.4 and 25.5 matter as much as “25.0.0”
Node 25 is a Current line (odd). It moves fast: first, new defaults land; then the ecosystem finds edge cases; then stabilization releases ship.[6]
• 25.0.0: V8 14.1, performance focus, web APIs, permission model, Web Storage enabled by default.[8]
• 25.2.0: the
localStorageregression without--localstorage-filesurfaces (nodejs/node #60704) and impacts toolchains.[5]• 25.4.0: require(esm) becomes stable, module compile cache becomes stable, ops goodies like
http.setGlobalProxyFromEnv()land.[9]• 25.5.0:
node --build-seaarrives — SEA “one step from core”, plus small but meaningful ops/monorepo changes.[12]
Quick terms so you don’t have to google every 2 minutes
Just enough vocabulary to make Node 25 readable.
• Current vs LTS: odd major = Current, even major = LTS line. Node explicitly recommends production use Active/Maintenance LTS.[6]
• Web Storage in Node: Node
localStorageis file-backed (--localstorage-file), unencrypted, has a 10MB quota, and is shared within a server process.[7]• Permission Model: process permissions (
--permission,--allow-net,--allow-fs-read) as a seatbelt. It’s not a sandbox and provides no guarantees against malicious code.[11]• require(esm): a bridge between CJS and ESM. Node 25.4.0 marks the mechanism stable (which changes migration strategy).[9]
• Module compile cache: on-disk code cache for speeding up module graph compilation, with a
portablemode.[10]• SEA: single executable applications. 25.5.0 makes this much easier with
--build-sea.[12][13]• Ops QoL: proxy from env (
http.setGlobalProxyFromEnv()), watcher ignore (fs.watch({ ignore: ... })).[14][15]
Node 25.0.0: what’s new “at the base”, and where the practical value is
25.0.0 is the foundation: V8 14.1, performance work, web APIs, and legacy cleanup. The real “maturity” shows up in 25.4/25.5.[8][9][12]
Web Storage
A bold default change that triggered ecosystem regressions (tools/tests/CLIs).[8][5][4]
Security posture
Permissions are framed as “secure-by-default”, with clear constraints (not a sandbox).[11]
Legacy cleanup
Major releases often hurt via deprecated API removal (SlowBuffer is a good example).[16]
A visual marker of the line start: Node.js 25.0.0 (Current).[8]
Section node-25-0 screenshotWeb Storage by default: why it blew up, and how it shows up in real projects
The core issue is not that Web Storage is “bad”. The issue is that the default behavior changed, and parts of toolchains started executing branches they never tested on the server.
nodejs/node #60704 describes the 25.2.0 regression: “Cannot initialize local storage without a --localstorage-file path”. Importantly, it references broken webpack/jest/html-webpack-plugin chains.[5]
Vitest issue #8757 shows another failure mode: in Node 25 localStorage is no longer undefined, which can break mocks in test environments.[4] Jest issue #15888 adds another data point: the test runner fails with the same SecurityError on Node 25.2.0.[3]
Fast incident workaround (when you don’t need storage): temporarily disable Web Storage via the flag documented in Node’s globals docs.[7]
Team lesson: even if “you don’t use localStorage”, your dependencies might. That’s why keeping Current in a CI matrix is a rational way to detect surprises early.[6]
nodejs/node #60704: Node 25.2.0 regression — touching localStorage without --localstorage-file can crash builds.[5]
Vitest #8757: Web Storage enabled by default in Node v25 can break tests due to changed localStorage expectations.[4]
Jest #15888: the runner fails with SecurityError on Node 25.2.0.[3]
Node 25.4.0: require(esm) becomes stable (and that changes how you migrate)
This is the kind of change that doesn’t scream in headlines, but saves weeks in repos where CJS and ESM must coexist.
1) Why it matters
2) Practical pattern: default export adapter
const pkg = require('some-esm-only-package');
const api = pkg?.default ?? pkg;
module.exports = api;This isn’t a “hack”. It’s a standard way to handle the namespace object when ESM provides a default export.[22]
3) What it signals for teams
Stabilization in Current is preparation for the broader ecosystem. Keeping LTS+Current in CI lets you capture benefits without taking prod risk.[6]
A visual marker of a “stabilization” point: Node.js 25.4.0 (Current).[9]
Section require-esm screenshotModule compile cache: portable mode and “grown-up” docs — what it gives you in CI/containers
Compile cache is an optimization. It shines when you have many cold starts or a large module graph (tools, CLIs, workers).
The node:module docs include a very practical portability section: if absolute paths change, cache effectiveness can drop; portable mode helps reuse in more scenarios.[10]
A minimal experiment setup:
import module from 'node:module';
module.enableCompileCache({ directory: '.node-compile-cache', portable: true });If you test this in CI, manage the cache directory explicitly so it survives between runs.
Docs: “Portability of the compile cache” + portable mode with API and env examples.[10]
Node 25.5.0: `node --build-sea` makes SEA a “single-step” workflow
This is one of the most product-level changes in the line: building a single executable no longer looks like an external-injector ritual.
1) The core idea
2) Practical takeaway
For CLIs and local agents: fewer dependencies and fewer brittle release steps. But test on target OS/shells (there are reports about Windows Command Prompt quirks).[21]
3) Team advice
SEA is a distribution tool, not a security model. Don’t use it as a substitute for dependency updates and supply-chain hygiene. Keep Current in CI to catch surprises early.[6]
25.5.0: the --build-sea section and the command example in the official release notes.[12]
Ops QoL: proxy from env and a clean path for corporate networks
Not headline features — but they reduce “tiny custom wrappers” in projects with corporate CI constraints.
Minimal snippet:
import http from 'node:http';
const restore = http.setGlobalProxyFromEnv();
// restore();API
Useful when proxies are configured via env (CI/enterprise networks).[14]
Outcome
Less chance each HTTP client ends up configured differently.
Docs: http.setGlobalProxyFromEnv([proxyEnv]) + built-in proxy support section.[14]
Monorepo pain: `fs.watch({ ignore })` as a simple way to reduce watcher noise
In large repos, watchers are their own world of pain (node_modules, .git, generated dirs). The docs describe ignore as an option that can take glob/RegExp/function/array.[15]
A minimal example for dev tools:
import { watch } from 'node:fs';
watch('.', {
recursive: true,
ignore: ['**/node_modules/**', '**/.git/**'],
}, () => {});Breaking changes: how Node 25 breaks deps via “legacy cleanup” (not new features)
Major Node releases often hurt not via brand-new APIs but via deprecation finalization. SlowBuffer is a clean example.[16]
1) What happened to SlowBuffer
Node’s deprecations docs state SlowBuffer was removed and recommend Buffer.allocUnsafeSlow(size).[16]
2) How it looks “in the wild”
There’s a real case of a backend crashing on Node 25.x due to a dependency chain touching SlowBuffer.prototype.[17]
3) Quick pre-upgrade scan
rg -n "\bSlowBuffer\b" .Then pnpm why / npm ls to identify the legacy dependency chain.
Upgrade playbook: how to adopt Node 25 for value (not pain)
A short “battle-tested” checklist teams actually use.
• 1) Keep prod on LTS, add Node 25 in CI (LTS + Current matrix).[6]
• 2) Run tests with Web Storage disabled to quickly isolate the source:
NODE_OPTIONS="--no-experimental-webstorage".[7]• 3) If it broke, look where it already broke: nodejs/node #60704, Vitest #8757, Jest #15888.[5][4][3]
• 4) If you want 25.4/25.5 features, adopt them first in a separate experiment (CLI/worker), not by flipping prod runtime in one go.[9][12]
• 5) For monorepos, tune watchers:
fs.watch({ ignore })reduces noise and load.[15]• 6) For corporate CI behind proxies, try
http.setGlobalProxyFromEnv()as a simple global approach.[14]
Verdict: should you upgrade to Node.js 25 now?
No ideology: Node 25 is great as a “future breakage detector” in CI, and it’s useful for certain products (CLIs/agents). But for production backends, LTS often wins on stability alone.
Production backend
Node recommends production run on Active/Maintenance LTS.[6]
CI / compatibility
Node 25 surfaces dependency regressions (Web Storage) and legacy cleanup (SlowBuffer) without prod incident risk.[5][16]
CLIs / agents
--build-sea (25.5.0) + 25.4.0 maturity are real DX wins. Test on target OS/shells.[12][21]
Mentally
Current exists so the ecosystem can adapt. If it breaks your day, keep it in CI and keep prod on LTS.[6]
Sources
Sources in order of appearance (position in list = footnote number [n]).
• Docusaurus issue #11545: build broken on Node 25.2.0 due to localStorage SecurityError Read source ↗
• Shopify community: shopify-cli fails on Node 25.2.0 due to localStorage SecurityError Read source ↗
• Jest issue #15888: Jest fails with localStorage error on Node 25.2.0 Read source ↗
• Vitest issue #8757: Node v25 breaks tests with Web Storage API Read source ↗
• nodejs/node issue #60704: Regression 25.2.0 — Cannot initialize local storage without --localstorage-file path Read source ↗
• Node.js Releases: Current vs LTS policy (production guidance) Read source ↗
• Node.js Globals docs: Web Storage semantics + disable flag Read source ↗
• Node.js 25.0.0 release notes (V8 14.1, Web Storage default, performance highlights) Read source ↗
• Node.js 25.4.0 release notes (require(esm) stable, compile cache stable, ops improvements) Read source ↗
• Node.js node:module docs: module compile cache + portability Read source ↗
• Node.js Permissions docs: seat belt approach (not a sandbox) Read source ↗
• Node.js 25.5.0 release notes (--build-sea, other notable changes) Read source ↗
• Node.js Single Executable Applications docs (--build-sea flow and config) Read source ↗
• Node.js HTTP docs: http.setGlobalProxyFromEnv() Read source ↗
• Node.js Deprecations docs: SlowBuffer removed + migration note Read source ↗
• twentyhq/twenty issue: crash on Node 25 due to SlowBuffer removal via dependency chain Read source ↗
• nodejs/Release issue #1113: cadence discussion (annual majors / LTS duration) Read source ↗
• nodejs/node issue: --build-sea fails in Windows Command Prompt (shell-specific behavior) Read source ↗
• Joyee Cheung: require(esm) in Node.js — from experiment to stability Read source ↗
• Joyee Cheung: Improving SEA building (why build moved into Node.js core) Read source ↗
FAQ
No. It’s Current (odd major). Node explicitly states production apps should use Active or Maintenance LTS.[6]
Because the default changed. `localStorage` became available without an experimental flag, but Node’s semantics differ (file, 10MB, shared). Tooling assumptions broke.[7][5][4]
Temporarily disable Web Storage via `NODE_OPTIONS="--no-experimental-webstorage"` or a runtime flag. It’s documented in globals and referenced as a workaround in regression threads.[7][5]
Yes, but test on target OS/shells. There’s a Windows Command Prompt specific issue.[21] For config/process details see SEA docs.[13]
In one sentence: take the value of Node 25 without taking its prod risk head-on
The healthiest strategy for most teams: keep prod on LTS, run Node 25 in CI as an early regression detector, and adopt 25.4/25.5 features (require(esm), compile cache, build-sea) first in isolated experiments (CLI/worker). You get the DX upside without “the day localStorage broke the build”.