
Published August 9, 2026 · 13 min read
JavaScript has been shipping a run of unusually practical features. Set algebra is now built in. Iterators can be transformed lazily. Typed arrays can convert base64 and hexadecimal data without detouring through binary strings. A callback that might return, throw, or produce a promise can be handled through one predictable interface.
The larger additions are even more interesting. Explicit resource management gives files, connections, locks, and subscriptions deterministic cleanup. Temporal replaces the overloaded Date mental model with separate types for instants, calendar dates, wall-clock times, and zoned date-times.
This guide starts with features that have reached current versions of the major browsers and ends with Stage 4 additions that are still rolling out. Compatibility notes were rechecked against TC39 and MDN on September 6, 2026. Treat the support notes as a starting point, then test the browsers, webviews, and server runtimes your product actually supports.
An ECMAScript edition and browser availability answer different questions. The edition says which annual specification snapshot owns a feature. Browser support tells you whether your users can execute it. TC39's finished-proposals list assigns the features below to ES2025, ES2026, or the expected ES2027 snapshot, but several shipped in browsers well before their publication year.
MDN's Baseline label means a feature works in the latest releases of a defined set of major browsers. It does not mean every active device, embedded webview, or Node.js release has caught up.
| Feature | Edition | Browser rollout signal |
|---|---|---|
New Set methods | ES2025 | Baseline since June 2024 |
| Iterator helpers | ES2025 | Baseline since March 2025 |
Promise.try() | ES2025 | Baseline since January 2025 |
| Import attributes | ES2025 | Baseline since April 2025; module types vary |
RegExp.escape() | ES2025 | Baseline since May 2025 |
Array.fromAsync() | ES2026 | Widely available since January 2024 |
Error.isError() | ES2026 | Limited availability |
Map.getOrInsert() | ES2026 | Baseline since February 2026 |
Uint8Array base64 and hex methods | ES2026 | Baseline since September 2025 |
Math.sumPrecise() | ES2026 | Baseline since April 2026 |
| Explicit resource management | Expected ES2027 | Limited availability |
| Temporal | Expected ES2027 | Shipping, but not yet universal |
Finding common or exclusive values used to mean converting collections to arrays and combining filter(), includes(), and extra allocations. Set now has seven methods that express the operation directly. The four composition methods return a new Set; the three relationship methods return a boolean. None mutate either input.
const a = new Set([1, 2, 3, 4])
const b = new Set([3, 4, 5, 6])
a.union(b) // Set { 1, 2, 3, 4, 5, 6 }
a.intersection(b) // Set { 3, 4 }
a.difference(b) // Set { 1, 2 }
a.symmetricDifference(b) // Set { 1, 2, 5, 6 }
a.isSubsetOf(b) // false
a.isSupersetOf(b) // false
a.isDisjointFrom(b) // falseThe methods reached Baseline in June 2024, and their names map cleanly to the underlying set operations. The important detail in the Set method contract is that the argument must be a Set or a set-like object.
Web platform objects such as GPUSupportedFeatures can be set-like even when they are not actual Set instances. The receiver on the left, however, must be a real Set.
Arrays have long supported map(), filter(), and reduce(). Iterators and generators did not, so developers either wrote loops or materialized an array before transforming the data.
Iterator helpers bring those chains to synchronous iterators while keeping transformations lazy:
function* numbers() {
let value = 0
while (true) {
yield value++
}
}
const squares = numbers()
.filter((value) => value % 2 === 0)
.map((value) => value * value)
.take(5)
.toArray()
console.log(squares) // [0, 4, 16, 36, 64]filter(), map(), flatMap(), drop(), and take() return helper iterators and do work only as values are requested. Terminal methods such as toArray(), reduce(), forEach(), some(), every(), and find() consume the iterator. That is why take(5) can safely stop an infinite generator before toArray() materializes the result. MDN marks Iterator.prototype.toArray() and the helper family as Baseline since March 2025.
Array iterators inherit the helpers too, so .values() is enough to start a lazy chain:
const squares = [1, 2, 3, 4, 5, 6, 7, 8]
.values()
.filter((value) => value % 2 === 0)
.map((value) => value * value)
.toArray()
console.log(squares) // [4, 16, 36, 64]These helpers are for synchronous iterators. Async iterator helpers remain separate work, so do not assume an async generator has the same methods.
Promise.try() is useful at an API boundary where a callback might return a plain value, throw synchronously, or return a promise. All three cases become one promise that callers can handle consistently.
function runTask(task) {
return Promise.try(task)
.then((result) => ({ ok: true, result }))
.catch((error) => ({ ok: false, error }))
}
await runTask(() => "ready")
await runTask(() => {
throw new Error("Synchronous failure")
})
await runTask(async () =>
fetch("/api/status").then((response) => response.json())
)The distinction is easy to miss. Promise.resolve(task()) cannot convert a synchronous throw because task() runs before Promise.resolve() receives a value. By contrast, a throw inside .then(() => task()) is already converted to a rejection; Promise.try() is a direct, readable way to start that normalization without creating a placeholder promise chain. It also forwards optional arguments to the callback, as the Promise.try() reference documents.
Native JSON module imports use an attribute to declare what the loader should accept:
import config from "./config.json" with { type: "json" }
console.log(config.version)Dynamic imports accept the attributes in an options object. Because import() resolves to a module namespace object, destructure its default export if you want the JSON value directly:
const { default: config } = await import("./config.json", {
with: { type: "json" },
})The attribute is more than a file-extension hint. On the web, a server must return an appropriate JSON MIME type; the import fails rather than executing the resource as JavaScript if the response type does not match. The import attributes documentation also covers CSS and text module types, but availability depends on the host and module type. JSON is the portable case to reach for first, and the older assert { type: "json" } spelling should not be used for new code.
Dynamic regular expressions are fragile when input contains ., *, +, ?, brackets, parentheses, or other syntax. RegExp.escape() produces text that can be inserted into a pattern as a literal value.
function createLiteralSearch(query) {
return new RegExp(RegExp.escape(query), "i")
}
const pattern = createLiteralSearch("gmail.com")
pattern.test("support@gmail.com") // true
pattern.test("support@gmailXcom") // falseThis is safer than maintaining a hand-written replacement list. The algorithm handles leading alphanumeric characters, punctuators, whitespace, lone surrogates, and embedding next to other escape sequences—edge cases a simple replaceAll() solution usually misses. RegExp.escape() has been Baseline since May 2025.
It makes text literal inside a regular expression; it is not a general-purpose sanitizer for HTML, URLs, SQL, or any other context.
Array.fromAsync() is the async counterpart to Array.from(). It accepts async iterables, synchronous iterables, and array-like values, then returns a promise for the completed array.
async function* fetchItems(url) {
let page = 1
while (true) {
const response = await fetch(`${url}?page=${page}`)
if (!response.ok) throw new Error(`Page request failed: ${response.status}`)
const data = await response.json()
if (data.items.length === 0) return
yield* data.items
page += 1
}
}
const allItems = await Array.fromAsync(fetchItems("/api/items"))Yielding each item matters. If the generator yielded data.items, the result would be an array of page arrays rather than one flat array of items.
The method follows for await...of-style iteration and accepts an optional async mapping function. It consumes sequentially rather than starting all work concurrently like Promise.all(). That is helpful for streams and ordered producers, but it is not a concurrency primitive. It also materializes the entire result, so keep processing as an async iterable when the source may be unbounded or too large for memory. MDN records Array.fromAsync() as widely available in browsers since January 2024 even though TC39 assigns its final specification work to ES2026.
instanceof Error relies on prototype identity. An error created in an iframe or another JavaScript realm has a different Error.prototype, so the check can return false for a genuine error.
Error.isError() performs a branded check instead:
function toError(value) {
return Error.isError(value) ? value : new Error(String(value))
}
Error.isError(new Error("oops")) // true
Error.isError(new TypeError("bad input")) // true
Error.isError({ message: "looks similar" }) // false
Error.isError("just a string") // falseIt also recognizes cross-realm errors and DOMException values while rejecting objects that merely inherit from Error.prototype. That makes Error.isError() the right eventual replacement for most branding checks, but it still has limited browser availability as of this review. Feature-detect it or keep a compatibility path for now.
Building a multimap or cache often starts with has(), followed by set(), followed by get(). Map.prototype.getOrInsert() turns that into one operation:
const membersByTeam = new Map()
for (const member of members) {
membersByTeam.getOrInsert(member.team, []).push(member)
}If the key exists, the method returns its value. Otherwise it inserts and returns the supplied default. The default expression is still evaluated before the call, even when the key already exists. For expensive values, use the computed variant:
const permissions = cache.getOrInsertComputed(userId, () =>
loadPermissions(userId)
)The callback runs only for a missing key and receives that key as its argument. MDN marks Map.prototype.getOrInsert() and getOrInsertComputed() as Baseline since February 2026. Baseline is a browser signal; check server runtimes separately.
Binary data used to require atob(), btoa(), Buffer-specific APIs, or a library—often with awkward conversions through strings. ES2026 adds a focused family of typed-array methods:
Uint8Array.fromBase64() and Uint8Array.prototype.toBase64();Uint8Array.fromHex() and Uint8Array.prototype.toHex();setFromBase64() and setFromHex() for decoding into an existing buffer.const bytes = Uint8Array.fromHex("deadbeef")
bytes.toBase64() // "3q2+7w=="
bytes.toHex() // "deadbeef"
const decoded = Uint8Array.fromBase64("3q2+7w==")
decoded.toHex() // "deadbeef"The base64 methods support the standard and URL-safe alphabets, plus controls for handling the final chunk. The setFrom... variants are especially useful for stream decoding because they report how much input was consumed and how many bytes were written. The Uint8Array.fromBase64() reference links the complete family, which reached browser Baseline in September 2025.
These APIs do not encode arbitrary text by themselves. Convert text with TextEncoder first and decode it with TextDecoder afterward.
Naive addition rounds after every operation. When values have very different magnitudes, a small value can disappear before later terms bring the total back into range:
const values = [1e20, 0.1, -1e20]
values.reduce((sum, value) => sum + value, 0) // 0
Math.sumPrecise(values) // 0.1Math.sumPrecise() behaves as though it sums the exact mathematical values represented by the input floats and rounds once at the end. That makes it materially more accurate for aggregation, but it does not turn binary floating point into decimal arithmetic:
Math.sumPrecise([0.1, 0.2]) // 0.30000000000000004The literals are already approximations, so exact decimal money still needs an integer-minor-unit or decimal-number strategy. Math.sumPrecise() reached Baseline in April 2026 and returns -0 for an empty iterable, another edge case worth covering in numeric code.
TC39 Stage 4 means the language design is finished and has interoperable implementations. It does not mean the feature is already present in every runtime your application targets. Explicit resource management and Temporal are expected in ES2027, and both deserve adoption plans rather than unguarded assumptions.
Files, database connections, stream readers, locks, event subscriptions, and timers often need explicit cleanup. Garbage collection cannot tell when an external resource must be released, and try...finally becomes noisy when several resources depend on one another.
A using declaration registers an object's [Symbol.dispose]() method for synchronous cleanup when the surrounding lexical scope exits:
class DatabaseConnection {
static open() {
const connection = new DatabaseConnection()
connection.connect()
return connection
}
connect() {
// Open the connection.
}
query(sql) {
// Return fully materialized rows.
}
close() {
// Close the connection.
}
[Symbol.dispose]() {
this.close()
}
}
function loadUsers() {
using connection = DatabaseConnection.open()
return connection.query("SELECT * FROM users")
}Cleanup is deterministic: it runs when control leaves the scope through normal completion, return, or an exception. It is not tied to garbage collection. Multiple resources are disposed in reverse declaration order, and disposal failures are preserved through SuppressedError rather than preventing the remaining cleanup work. MDN's using reference documents the scope and error rules.
Asynchronous resources use await using and [Symbol.asyncDispose]():
async function readConfig() {
await using file = await ManagedFile.open("config.json")
// `await` ensures the read finishes before scope-exit cleanup starts.
return await file.readText()
}DisposableStack and AsyncDisposableStack cover ad hoc cleanup when changing the resource's class is impractical. They can register existing disposables or defer arbitrary cleanup callbacks.
TypeScript has understood this syntax since TypeScript 5.2, but compilation support is not the same as native runtime support. Depending on the output target and features used, an application may still need helpers or polyfills for the disposal symbols, stacks, and SuppressedError. Browser support for the native feature remains limited.
Date makes one mutable object carry several meanings: an instant, a local display, a calendar date, and sometimes a makeshift duration. Temporal separates those concepts:
Temporal.Instant represents one exact point on the timeline;Temporal.PlainDate, PlainTime, and PlainDateTime deliberately have no time zone;Temporal.ZonedDateTime combines an instant, named time zone, and calendar;Temporal.Duration represents an amount of time.That makes ordinary domain code easier to state correctly:
const birthday = Temporal.PlainDate.from("1990-05-15")
const today = Temporal.Now.plainDateISO()
const age = birthday.until(today, { largestUnit: "years" })
console.log(`${age.years} years old`)Temporal objects are immutable, and arithmetic follows the semantics of the type being used. Adding one day to a zoned date-time is a calendar operation; adding 24 hours is an elapsed-time operation, and those can differ across daylight-saving transitions.
The Temporal proposal is Stage 4 and records native releases in Firefox 139, Chrome 144, and Node.js 26. Safari is not yet listed as shipped, so a browser application still needs a measured fallback policy. A production polyfill can make adoption possible sooner, but its bundle cost, time-zone behavior, and runtime coverage belong in the decision.
For the complete type model, compatibility detail, and a boundary-first migration plan, read JavaScript Temporal: Choosing the right date and time types.
Temporal can replace many date-manipulation dependencies, but not every specialized time library. Locale formatting still belongs with Intl, recurrence rules remain their own domain, and integrations may continue to exchange Date, epoch values, or strings.
The safest approach is not “use ES2027 everywhere” or “wait until every old browser disappears.” Treat each feature according to how it enters your program.
typeof Math.sumPrecise === "function" work for methods, while syntax such as using must be handled by your compiler and build target before an older parser sees it.Date boundaries.The small APIs can earn their place one expression at a time. The larger ones deserve architecture-level intent.
The most valuable recent JavaScript features are not ornamental syntax. They encode operations developers already perform—set math, lazy iteration, sync-or-async normalization, binary conversion, accurate aggregation, cleanup, and time modeling—with fewer opportunities for accidental behavior.
Use Set methods, iterator helpers, Promise.try(), import attributes, and RegExp.escape() as ordinary tools once they fit your support matrix. Evaluate the ES2026 APIs per runtime rather than assuming browser Baseline covers the backend. Treat explicit resource management and Temporal as finished designs with incomplete deployment, and introduce them behind deliberate build, fallback, and boundary strategies.
That distinction—standardized versus available—is the habit worth carrying forward. It lets you benefit from modern JavaScript without turning your users into compatibility testers.
Set.prototype.union() and the Set method familyIterator.prototype.toArray() and iterator helpersPromise.try()RegExp.escape()Array.fromAsync()Error.isError()Map.prototype.getOrInsert()Uint8Array.fromBase64() and related binary conversionsMath.sumPrecise()using declarationsOccasional articles on React, full-stack development, performance, and AI workflows.