set.union is not a function: Why the New JavaScript Set Methods Fail in Node 20, Older Browsers and Some TypeScript Setups, and How to Fix It
TypeError: set.union is not a function (or intersection, difference, isSubsetOf) means the engine predates the ES2025 Set methods: Node 20 and below, Chrome before 122, Firefox before 127, Safari before 17. How to check, the one-line fallbacks, the core-js polyfill, and the TypeScript lib fix.
You replaced a clumsy spread-and-filter with a.union(b) or allowed.isSubsetOf(granted), the tests passed on your laptop, and then a CI job on Node 20, an older Safari, or a Jest run with a jsdom environment threw TypeError: a.union is not a function. The code is correct; the engine is old. This post explains exactly which engines are missing the methods, how to tell which one you hit, and the four ways to fix it, from a one-line fallback to a proper polyfill.
It is the companion to our guide to the Set methods and to comparing sets for equality and subsets, and it follows the same shape as the fix posts for Object.groupBy, toSorted and Promise.withResolvers.
What the error actually means
ES2025 added seven methods to Set.prototype: union, intersection, difference, symmetricDifference, isSubsetOf, isSupersetOf and isDisjointFrom. They shipped in Chrome and Edge 122, Safari 17, Firefox 127 and Node.js 22, and became Baseline newly available in June 2024 when Firefox caught up. Any engine older than that has a perfectly good Set with no union on it, so the property lookup returns undefined and calling undefined produces TypeError: a.union is not a function.
Two details matter for debugging. First, the error names whichever method you called first, so you may see intersection is not a function or isSubsetOf is not a function for the same root cause. Second, bundlers do not help here. Babel, esbuild, SWC and TypeScript transform syntax, not built-in methods: a target of es2020 happily emits a.union(b) unchanged and leaves the runtime to fail. Only a polyfill or a newer engine fixes it.
Step 1: find out which environment is missing it
The usual suspects, roughly in the order they show up in bug reports:
- Node.js 20 or earlier. Lambda, Cloud Functions, Docker images pinned to
node:20, and Jest workers all run whatever Node the host has. Node 20 is still common in 2026 even though it has left active support. Runnode -e "console.log(process.version, typeof new Set().union)"on the failing machine. - Safari 16 and earlier, Chrome before 122, Firefox before 127. The browsers are mostly updated by now, but corporate fleets and older iPads are not. Check your error-reporting tool for the user agent on the failing event.
- Jest with the jsdom environment on an old Node. jsdom does not add built-ins; the Node version underneath decides. The fix is the same as for Node.
- React Native and Hermes. Hermes gained the Set methods relatively late; check the release notes of the Hermes version your React Native pins rather than assuming.
- Only the type checker complains. If the code runs but
tscsaysProperty 'union' does not exist on type 'Set<string>', that is alibsetting, covered below, not a runtime problem.
// Feature test you can run anywhere
const hasSetMethods = typeof Set.prototype.union === 'function';
console.log(hasSetMethods ? 'native Set methods' : 'missing: polyfill or fallback needed');Fix 1: move the engine forward
If you control the runtime, this is the right fix and the only one that removes code instead of adding it. Node 22 and Node 24 both have the methods, and Node 26 is the current LTS line. Bump the engines field in package.json, the base image, the .nvmrc and the CI matrix together, so the next person who hits the error gets a clear message instead of a stack trace.
{
"engines": { "node": ">=22" }
}For browsers, check what your analytics say about Safari 16 and Chrome 121 traffic. If it rounds to zero, update your browserslist and move on; if it does not, you need one of the next three fixes.
Fix 2: the one-line fallbacks
Every Set method has a short equivalent using spread and filter. They are slower on large sets because they create intermediate arrays, but for a few hundred elements nobody will notice, and they work in every engine that has Set at all.
const union = (a, b) => new Set([...a, ...b]);
const intersection = (a, b) => new Set([...a].filter((x) => b.has(x)));
const difference = (a, b) => new Set([...a].filter((x) => !b.has(x)));
const symmetricDifference = (a, b) =>
new Set([...[...a].filter((x) => !b.has(x)), ...[...b].filter((x) => !a.has(x))]);
const isSubsetOf = (a, b) => [...a].every((x) => b.has(x));
const isSupersetOf = (a, b) => isSubsetOf(b, a);
const isDisjointFrom = (a, b) => ![...a].some((x) => b.has(x));Use these when the engine is old and you cannot change it this quarter, or when you only have one or two call sites. Put them in a small sets.js helper, export them, and leave a comment pointing at the engine version that makes them deletable.
Fix 3: a real polyfill with core-js
If the methods are used across a codebase, or you want the native call sites to stay as they are, install core-js and import the Set polyfills once at the entry point. core-js installs the methods on Set.prototype only when they are missing, so modern engines keep their native, faster implementation.
// npm install core-js
// At the very top of your entry file:
import 'core-js/actual/set';
// Or only the methods you use:
import 'core-js/actual/set/union';
import 'core-js/actual/set/is-subset-of';In a Babel-based front-end build, @babel/preset-env with useBuiltIns: 'usage' and corejs: 3 will inject exactly these imports where the methods are called, based on your browserslist, and will stop injecting them automatically once your support matrix moves past Firefox 127. That is the most maintainable option for a browser app with a real support policy.
One caveat: the native methods are strict about what they accept. The argument must be set-like, meaning it has a numeric size, a has method and a keys method; a plain array is rejected with its own TypeError. The spread fallbacks above silently accept arrays. If you polyfill, keep passing real Sets so behaviour does not change when the polyfill is removed.
Fix 4: the TypeScript lib setting
If the runtime is fine but the compiler reports Property 'union' does not exist on type 'Set<number>', the types for the Set methods are not in your lib. TypeScript 5.5 added them under esnext.collection, which the esnext lib includes. Either move lib to esnext, or add the collection lib explicitly without changing what you emit:
{
"compilerOptions": {
"target": "es2022",
"lib": ["es2022", "dom", "esnext.collection"]
}
}Remember that fixing the types does nothing for an old runtime; if both are old, you need Fix 1 or Fix 3 as well. The reverse mistake is also common: adding esnext.collection makes the compiler happy on a project that still deploys to Node 20, and the error moves from your editor to production.
Which fix to choose
- A service you deploy yourself: upgrade Node and pin
engines. Nothing else to maintain. - A library published to npm: avoid the methods in your own source, or use the spread helpers internally, and declare the engine requirement in
enginesand the README. Do not ship core-js inside a library. - A front-end app with a browserslist: core-js through Babel's
usagemode, so the polyfill disappears on its own. - One or two call sites, old engine, no time: the helper functions, with a comment that says when to delete them.
- Only the type checker complains: add
esnext.collectiontoliband move on.
The Set methods are among the nicest additions to the language in years, and they are safe to use in any environment from mid-2024 onwards. When set.union is not a function appears, it is almost always a single old runtime in the chain, and the right response is to find it, name it in your docs, and either move it forward or carry the method with you until you can.