Back to Blog
Features2026-04-30

ES2026 Features Overview: What is New in JavaScript

A practical look at the ES2026 additions every JavaScript developer should know.

ES2026 lands with a tighter, more pragmatic feature set than recent years. The proposals reaching Stage 4 focus on ergonomics for async code, immutable data, and developer-friendly iteration helpers. This overview covers what each feature actually does, the edge cases that will bite early adopters, and a sane adoption order.

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.