ES2026 Features Overview: What is New in JavaScript
A practical look at the ES2026 additions every JavaScript developer should know.
Iterator Helpers Reach Maturity
The iterator helpers proposal finally ships everywhere: map, filter, take, drop, flatMap, reduce, toArray, forEach, some, every, and find are now methods on every iterator. You can compose lazy pipelines without pulling in lodash:
- const evens = numbers.values().filter(n => n % 2 === 0).map(n => n * 2).take(10).toArray();
The crucial difference from array methods is laziness. Nothing executes until a consuming method (toArray, reduce, forEach, or a for...of loop) pulls values through the pipeline. Each element flows through the entire chain one at a time, so no intermediate arrays are materialized — which matters once you push past 100k elements or process an infinite generator.
Two edge cases to internalize before you refactor:
- Iterators are single-use. Calling toArray() on a helper chain consumes the underlying iterator; a second call returns an empty result rather than re-running the pipeline. Arrays can be re-iterated, iterators cannot.
- Helpers close the source on early exit. When take(10) finishes, it calls return() on the upstream iterator, which matters if the source holds a resource (a file handle, a DB cursor wrapped in a generator with finally).
Records and Tuples
After years at Stage 2, immutable records and tuples landed using the #{ a: 1 } and #[1, 2, 3] syntax. They are deeply immutable primitives, not objects, and that changes the semantics in useful ways:
- Compared by value. #[1, 2] === #[1, 2] is true. Reference identity is gone; structural equality is the identity.
- Usable as Map keys and Set members. A Map keyed by #[x, y] coordinates simply works — no more serializing keys with JSON.stringify and praying key order stays stable.
- Only primitives and other records/tuples inside. Placing a function, Date, or plain object in a record throws a TypeError at construction time. This is the main migration friction: your "config object" often contains a callback, and that can never become a record.
For React and Redux users, value semantics mean memoization keys and dependency arrays stop producing false negatives caused by fresh object identities.
Set Methods Everywhere
Browsers and Node 24+ now ship union, intersection, difference, symmetricDifference, isSubsetOf, isSupersetOf, and isDisjointFrom on Set.prototype. Points that the MDN examples gloss over:
- They accept any set-like argument (anything with size, has, and keys), not just Sets. A Map qualifies — its keys act as the set.
- They return new Sets and never mutate the receiver, so chaining is safe: a.union(b).difference(c).
- Complexity is proportional to the smaller operand for intersection, so put the big collection on the receiver side when sizes are lopsided.
Delete the polyfills; the native versions are faster and correctly handle the set-like protocol.
Promise.try and AsyncContext
Promise.try(fn) removes the awkward dance of wrapping synchronously-throwing functions in promise chains. Promise.try(parseConfig) catches a synchronous throw from parseConfig and turns it into a rejection, while Promise.resolve(parseConfig()) would throw before the promise machinery ever engages. If the function returns a promise, it is adopted as-is; if it returns a value, you get a fulfilled promise. It is the correct first line of any promise-returning API boundary that calls user-supplied callbacks.
AsyncContext (Stage 3, already shipping in Node 24 behind a flag) gives you per-request context that survives await boundaries. Think of it as AsyncLocalStorage standardized for every runtime: a context.run(value, fn) call makes context.get() return that value anywhere in the async call tree below fn, including after awaits, timers, and promise callbacks. This is the primitive that makes tracing, request-scoped logging, and OpenTelemetry instrumentation work without monkey-patching every async API in existence.
Common Mistakes During Adoption
- Treating iterator helper chains as reusable values. Assign the *source* array, not the iterator, if you need to run the pipeline twice.
- Reaching for records everywhere. They are for data; anything with methods, mutation, or class instances stays an object. Mixed trees (objects containing records) are fine and normal.
- Assuming Set methods mutate like add and delete do. They do not; ignoring the return value is a no-op.
- Shipping syntax without checking your toolchain. #{} and #[] require up-to-date parser support in TypeScript, Babel, and esbuild — older versions hard-fail on the syntax, they do not degrade gracefully.
What to Adopt First
Start with iterator helpers and Set methods — they offer immediate readability wins, zero behavioral surprises in typical code, and trivial rollback if a target environment lags. Promise.try is a safe drop-in wherever you wrote the new Promise(resolve => resolve(fn())) idiom. Records and tuples deserve a design conversation first: they reshape equality semantics, which is exactly why they are valuable and exactly why sprinkling them casually through a codebase creates two incompatible data styles. Pick the boundaries — cache keys, state snapshots, coordinates — and adopt deliberately.