Skip to the chapter
←JavaScriptGGroundworkJSJavaScript
JavaScript/Intermediate/I11
28 / 41 written chapters
/

The cover
  1. B1Setup & mental model
  2. B2Execution context
  3. B3var, let & const
  4. B4Scope
  5. B5Single thread
  6. B6In the browser
  7. B7Types & values
  8. B8Strings & Math
  9. B9Operators & flow
  10. B10Functions
  11. B11Objects & arrays
  12. B12this
  13. B13Closures
  14. B14DOM & events
  15. B15Callbacks & promises
  16. B16Errors & tools
  17. B17Guided project
  18. ★Cheat page
  1. I1Scope & functions, properly
  2. I2Objects deeply
  3. I3Prototypes & OOP
  4. I4Async, properly
  5. I5Modules & tooling
  6. I6Regex & dates
  7. I7Error handling
  8. I8Real-time connections
  9. I9Offline & storage
  10. I10Browser APIs
  11. I11Modern JS
  12. ★Cheat page
  1. A1Engine & memory
  2. A2Advanced async
  3. A3Metaprogramming
  4. A4Types & data
  5. A5Patterns & architecture
  6. A6Performance
  7. A7Security
  8. A8Ecosystem
  9. A9Testing
  10. A10Observability
  11. A11Interview bank
  12. A12Build it yourself
  13. ★Cheat page

JSI11 · 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.

7 min read8 sections2 exercises
28 of 41

In this chapter

  • 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

and 3 more

about 7 min
08On this page
  1. Two questions for every new feature
  2. Iterator helpers: lazy chains without an array
  3. Promise.try: one rule for sync and async failures
  4. RegExp.escape: user text inside a pattern
  5. Error.isError, and why instanceof is not enough
  6. Set methods and a few smaller ones
  7. Temporal, the one that changes daily work
  8. 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.

FeatureWhat it gives youSupport, in short
toSorted, toReversed, withArray methods that return a copyWidely available; covered earlier
Object.groupByGroup items by a keyWidely available
Set methods (union and friends)Set algebraAvailable in current browsers and Node 22
Iterator helpersLazy map/filter/take on any iteratorBaseline 2025; Node 22
Promise.tryStart a promise chain from any functionBaseline 2025; Node 23 and later
RegExp.escapeEmbed user text in a patternCurrent browsers; Node 24
Array.fromAsyncCollect an async iterableWidely available
Import attributesimport data from "./x.json" with { type: "json" }Baseline 2025
Float16ArrayHalf-precision numbersCurrent browsers
Error.isErrorReliable error check across realmsChrome and Firefox; check Safari
using / Symbol.disposeDeterministic cleanupChromium and Node; not everywhere
TemporalA correct date and time APIChrome 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() yieldspasses filter(n % 2 === 0)?map(n => n * n)take(5) collects
1no — dropped——
2yes41 of 5
3no — dropped——
4yes162 of 5
5no — dropped——
6yes363 of 5
7no — dropped——
8yes644 of 5
9no — dropped——
10yes1005 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 though naturals() 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.try fixes that a plain .catch() on a sometimes-sync function doesn't.
  • Explain why foreign instanceof Error can be false for a genuine error, and why Error.isError doesn't have that problem.
  • Explain why Temporal splits a date/time into PlainDate, Instant, ZonedDateTime, and Duration instead of one mutable object like Date.

02Practice this chapter

Opens in the editor: write it, run it, and check it against real tests.

  • 10 testsEscape text for a regular expressionintermediate
  • 10 testsImplement Promise.tryintermediate
All 33 for this level →
I11

Finished reading?

0 of 41 chapters read in this series

→Next stop

  1. I10Previous · I10Browser APIs, in depth
  2. I11you are here
  3. Next · A1Engine & memoryWhat V8 is actually doing while your code just runs.10 min read

01On this page

8 sections · 0%

  1. 01Two questions for every new feature
  2. 02Iterator helpers: lazy chains without an array
  3. 03Promise.try: one rule for sync and async failures
  4. 04RegExp.escape: user text inside a pattern
  5. 05Error.isError, and why instanceof is not enough
  6. 06Set methods and a few smaller ones
  7. 07Temporal, the one that changes daily work
  8. 08Using new features without breaking old browsers

[] chapters · t top