I11 · Part 2 · Intermediate
Modern JavaScript, ES2023 to ES2026
What the language gained in the last few years, and how to tell whether you can use it yet.
08On this page
- Two questions for every new feature
- Iterator helpers: lazy chains without an array
- Promise.try: one rule for sync and async failures
- RegExp.escape: user text inside a pattern
- Error.isError, and why instanceof is not enough
- Set methods and a few smaller ones
- Temporal, the one that changes daily work
- Using new features without breaking old browsers
Two questions for every new feature
A feature has a spec year and a support date, and they are different things. ES2025 means the committee finalised it in 2025; whether your users' browsers run it is a separate question. So for any feature there are two checks: is it in the language, and is it Baseline — working in the current version of every major browser — for the audience you have? "Newly available" Baseline means it just crossed that line; "widely available" means it has held for about two and a half years.
| Feature | What it gives you | Support, in short |
|---|---|---|
toSorted, toReversed, with | Array methods that return a copy | Widely available; covered earlier |
Object.groupBy | Group items by a key | Widely available |
Set methods (union and friends) | Set algebra | Available in current browsers and Node 22 |
| Iterator helpers | Lazy map/filter/take on any iterator | Baseline 2025; Node 22 |
Promise.try | Start a promise chain from any function | Baseline 2025; Node 23 and later |
RegExp.escape | Embed user text in a pattern | Current browsers; Node 24 |
Array.fromAsync | Collect an async iterable | Widely available |
| Import attributes | import data from "./x.json" with { type: "json" } | Baseline 2025 |
Float16Array | Half-precision numbers | Current browsers |
Error.isError | Reliable error check across realms | Chrome and Firefox; check Safari |
using / Symbol.dispose | Deterministic cleanup | Chromium and Node; not everywhere |
| Temporal | A correct date and time API | Chrome 144 and Firefox 139; Safari not in a stable release as of September 2026 |
Treat this table as a starting point, not a source of truth: support changes every few weeks. The pages that stay current are MDN (each page ends with a compatibility table) and the Baseline badges on MDN and web.dev.
Iterator helpers: lazy chains without an array
function* naturals() { let n = 1; while (true) yield n++; }
naturals()
.filter((n) => n % 2 === 0)
.map((n) => n * n)
.take(5)
.toArray(); // [4, 16, 36, 64, 100]
The same chain on an array would build a full intermediate array at every step,
and on an infinite source it would never finish. Iterator helpers
(map, filter, take, drop,
flatMap, reduce, toArray,
some, every, find) pull one value at a
time through the whole chain, and stop as soon as take is
satisfied. They work on generators, Map and Set
iterators, and anything wrapped with Iterator.from. The catch:
an iterator is consumed once, so a helper chain cannot be replayed.
Dry run: pulling values one at a time through the chain
naturals() yields | passes filter(n % 2 === 0)? | map(n => n * n) | take(5) collects |
|---|---|---|---|
| 1 | no — dropped | — | — |
| 2 | yes | 4 | 1 of 5 |
| 3 | no — dropped | — | — |
| 4 | yes | 16 | 2 of 5 |
| 5 | no — dropped | — | — |
| 6 | yes | 36 | 3 of 5 |
| 7 | no — dropped | — | — |
| 8 | yes | 64 | 4 of 5 |
| 9 | no — dropped | — | — |
| 10 | yes | 100 | 5 of 5 — satisfied |
naturals() never generates 11 — once take(5) has its
fifth value it stops pulling, and that stop propagates all the way back through
map and filter to the generator itself. An array-based
chain has no such brake: .filter().map() on an array would
materialize a full intermediate array at every step, which is impossible on an
infinite source in the first place.
Promise.try: one rule for sync and async failures
function load(input) {
return Promise.try(() => JSON.parse(input)) // a sync throw becomes a rejection
.then(save);
}
load("{ not json").catch(report); // the error arrives here, not as a thrown exception
A function that is sometimes synchronous and sometimes returns a promise is a
trap: a caller who writes .catch is not protected from the sync
throw. The older fixes each have a flaw.
Promise.resolve().then(fn) catches the error but delays running
fn to a later microtask; new Promise((res) =>
res(fn())) is correct but noisy. Promise.try(fn, ...args)
runs fn immediately and synchronously, and turns whatever
happens — a return value, a promise, a throw — into a promise.
RegExp.escape: user text inside a pattern
const term = "file (1).txt";
new RegExp(term).test("file 1.txt"); // true — the "." and ( ) were treated as syntax
new RegExp(RegExp.escape(term)).test("file 1.txt"); // false — matched literally
Building a regular expression from a search box without escaping is both a bug
(a stray ( throws) and a security problem (a crafted pattern can
backtrack catastrophically). Everyone used to paste the same
replace(/[.*+?^${}()|[\]\\]/g, "\\$&") helper. The built-in
version handles the edge cases that helper misses, including making the result
safe to place next to other pattern text; note that it may write a leading
letter or digit as a hex escape such as \x61, which looks odd and
matches the same thing. Regular expressions also gained inline modifiers, so
/^(?i:bearer) abc$/ is case-insensitive for just one group.
Error.isError, and why instanceof is not enough
const foreign = iframe.contentWindow.eval("new Error('x')");
foreign instanceof Error; // false — it came from another realm's Error
Error.isError(foreign); // true
Every iframe, worker and Node vm context has its own copy of
Error, so instanceof fails for errors that crossed a
boundary. Error.isError checks for the real internal error data,
not the prototype chain, so it is true for genuine errors from any realm and
false for a plain object that merely inherits from Error.prototype.
It matters most in code that receives errors from elsewhere: a logging library,
a test framework, a postMessage handler.
Set methods and a few smaller ones
const a = new Set([1, 2, 3]);
const b = new Set([3, 4]);
a.union(b); // Set {1, 2, 3, 4}
a.intersection(b); // Set {3}
a.difference(b); // Set {1, 2}
a.isSubsetOf(b); // false
await Array.fromAsync(asyncGenerator()); // an array of every yielded value, in order
new Float16Array([1.337])[0]; // 1.3369140625 — half precision
Set methods replace a loop or a spread-and-filter, and they return new sets.
Array.fromAsync awaits an async iterable one item at a time and
gives you a normal array. Float16Array stores numbers in 16 bits,
which is the point: graphics, GPU and machine-learning data where memory
matters more than precision. Explicit resource management
(using handle = open(), with Symbol.dispose) is
covered with metaprogramming.
Temporal, the one that changes daily work
const today = Temporal.Now.plainDateISO(); // a date, no time, no zone
today.add({ days: 30 });
const meeting = Temporal.ZonedDateTime.from("2026-03-07T09:00[America/New_York]");
meeting.add({ days: 1 }).toString();
// "2026-03-08T09:00:00-04:00[America/New_York]" — still 9:00 on the wall, offset changed
Date mixes an instant, a local time and a formatting locale in one
mutable object, which is where most date bugs come from. Temporal separates
them: PlainDate for a calendar date, Instant for an
exact moment, ZonedDateTime for a moment in a named time zone,
Duration for an amount. Arithmetic respects daylight saving, and
every object is immutable. Temporal reached the final stage of the standards
process in March 2026, and Chrome and Firefox ship it; Safari had not shipped
it in a stable release when this was written, so production code still uses the
@js-temporal/polyfill package. The
dates section shows why it exists.
Using new features without breaking old browsers
if (typeof Promise.try !== "function") {
Promise.try = (fn, ...args) => new Promise((resolve) => resolve(fn(...args)));
}
- New syntax (
using, import attributes, regex modifiers) is a parse error in an engine that does not know it, so it needs a transpiler for older targets. - New built-ins (
Promise.try, Set methods, iterator helpers) are just missing functions: feature-detect them, or load a polyfill such as core-js. Transpiling cannot add them. - Decide by your data. Set your build's browser targets from your own analytics rather than a guess, and let that decide which of the above needs help.
- Node counts too. The same feature can be safe in the browser and missing in the Node version you deploy to; check both.
Interview answer, one sentence
"A feature's spec year is not its support date, so I check MDN and Baseline for
my actual audience, feature-detect or polyfill new built-ins such as
Promise.try and iterator helpers, transpile new syntax such as
using, and reach for Temporal instead of Date where
time zones or daylight saving are involved."
Before you move on
- Explain why the
naturals().filter().map().take(5)chain never overflows, even thoughnaturals()yields forever. - Explain the difference between a feature's spec year and its Baseline support date, and where you'd check the latter for a real audience.
- Explain the specific bug
Promise.tryfixes that a plain.catch()on a sometimes-sync function doesn't. - Explain why
foreign instanceof Errorcan befalsefor a genuine error, and whyError.isErrordoesn't have that problem. - Explain why Temporal splits a date/time into
PlainDate,Instant,ZonedDateTime, andDurationinstead of one mutable object likeDate.
Opens in the editor: write it, run it, and check it against real tests.
All 33 for this level →Finished reading?
0 of 41 chapters read in this series