跪拜 Guibai
← Back to the summary

ES2026 Lands: 10 Features That Cut JavaScript Boilerplate in Half

Foreword

Hello everyone, I'm 秋天的一阵风

On June 30, 2026, Ecma International officially approved the ECMAScript 2026 language specification. This is the 17th version of JavaScript.

Looking back, it's quite interesting—ten years ago, ES6 added classes, arrow functions, and Promises to JS. Back then, everyone thought, "JS finally feels like a real language." Ten years later, with ES2026, the folks at TC39 didn't introduce any flashy new syntax. Instead, they honestly filled in the pitfalls that developers have been complaining about for a decade.

💡 Further Reading: What is TC39

TC39, short for Technical Committee 39, is the official technical committee ⚙️ under Ecma International, fully responsible for the design, review, and iteration of the ECMAScript (JavaScript) language specification. Its members include major browser vendors, front-end companies, and industry experts 👨‍💻. All new language features must go through the complete Stage 0 to Stage 4 proposal process 📑, and only after completing Stage 4 can they be formally included in the standard.

🔗 Direct Official Resources:

1. The using Keyword: Your Resources Are Now Automatically Released

How Painful It Was Before

Anyone who has written Node.js has definitely seen code like this:

// Old way: try-finally nesting
async function processFile(path) {
  let fileHandle;
  let dbConn;
  try {
    fileHandle = await fs.open(path, 'r');
    dbConn = await pool.getConnection();
    const data = await fileHandle.read();
    await dbConn.query('INSERT INTO logs VALUES (?)', [data]);
    return processData(data);
  } finally {
    if (fileHandle) await fileHandle.close();
    if (dbConn) await dbConn.end();
  }
}

It's this verbose for just two resources. In real projects, with file handles, database connections, network requests, distributed locks... four or five resources nested together, the finally block becomes longer than the business logic. Forgetting to close one means a memory leak in production.

How ES2026 Solves It

// ES2026: using keyword, auto-cleanup when leaving scope
async function processFile(path) {
  await using fileHandle = await fs.open(path, 'r');
  await using dbConn = await pool.getConnection();

  const data = await fileHandle.read();
  await dbConn.query('INSERT INTO logs VALUES (?)', [data]);
  return processData(data);
} // ← Curly brace ends, fileHandle and dbConn are automatically closed

The role of using can be summed up in one sentence: Declare it inside a set of curly braces, and it is automatically released when leaving those curly braces. Whether it's a return, a throw, or normal execution completion, it will clean up for you.

What the Principle Looks Like

Any object that implements [Symbol.dispose]() (sync) or [Symbol.asyncDispose]() (async) can work with using:

class FileHandle {
  #fd;

  constructor(path) {
    this.#fd = fs.openSync(path, 'r');
  }

  read() {
    return fs.readFileSync(this.#fd, 'utf-8');
  }

  [Symbol.dispose]() {
    // Automatically called when the using block ends
    fs.closeSync(this.#fd);
    console.log('File closed');
  }
}

// Clean and neat to use
{
  using handle = new FileHandle('./config.json');
  const content = handle.read();
  // do something...
} // ← Automatically calls [Symbol.dispose]()

With Multiple Resources, the Release Order is Stack-like

The last declared is the first released, like stacking plates:

async function uploadAndNotify(filePath, recipients) {
  await using stream = fs.createReadStream(filePath);  // Released third
  await using conn = await smtp.connect('smtp.example.com'); // Released second
  await using session = await conn.login(credentials); // Released first

  // Upload file stream...
  // Send notification email...
}
// No matter where an error occurs in the middle, the release order is always: session → conn → stream
// You won't encounter a situation where "the connection is broken but the file handle is still open"

Additionally, if the business logic throws an exception and dispose() itself also fails, ES2026 will package the dispose error as a SuppressedError attached to the main error—neither will be silently lost.

2. Error.isError(): Finally, No More instanceof Error

Determining if something is an Error has always been a bit of a dark art in JS:

// Problems with the old way
try {
  riskyOperation();
} catch (e) {
  if (e instanceof Error) {
    // ❌ Looks fine, but guess what—sometimes it's false
  }
}

Where's the problem? JS has more than one "world". In the browser, every tab opened, every iframe embedded, every Web Worker started is an independent little universe, each with its own complete set of Error, TypeError, RangeError constructors.

Imagine this: iframe A throws a TypeError, your main page catches it with try-catch, and then uses instanceof Error to check it—the result is false. Because it's an Error from the iframe's world, and although it looks exactly the same as the Error in your main page, it's simply not the same constructor.

// A runnable example 🌰
// An iframe is inserted into the page, get an Error from the iframe
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);

const iframeError = new iframe.contentWindow.Error('Error from iframe');
console.log(iframeError instanceof Error);  // false 😱
// It's clearly an Error, but instanceof says it's not

Error.isError() is here to clean up this mess—it doesn't care "which world's Error are you from," it only cares "are you an Error?":

// ES2026: Directly ask "Are you an Error?" without checking the household register
try {
  throw new TypeError('An error occurred');
} catch (e) {
  console.log(Error.isError(e));  // true ✅
}

// Errors from iframes can also be recognized
console.log(Error.isError(iframeError));    // true ✅

// Strings, objects, null all correctly return false
console.log(Error.isError('just a string'));   // false ✅
console.log(Error.isError({ message: 'x' }));   // false ✅
console.log(Error.isError(null));               // false ✅

Just one line of API, but it solves the "cross-realm authentication problem" of instanceof Error that has plagued front-end developers for a decade.

3. Math.sumPrecise(): Summing Large Numbers of Values No Longer Loses Precision

Let's be clear first—it cannot solve the 0.1 + 0.2 problem:

Math.sumPrecise([0.1, 0.2]);  // Still 0.30000000000000004 😅

Why? Because from the moment 0.1 and 0.2 are written, they are already imprecise approximations in IEEE 754. The "representation error" occurs before the addition; the Kahan algorithm can't manage that.

So what does it solve? Accumulation error. When you serially add a large number of floating-point numbers, each addition produces a tiny rounding error. A few numbers don't matter, but adding tens or hundreds together will cause the error to accumulate to a visible level.

// Summing a large number of values: reduce fails, sumPrecise holds steady
const prices = Array.from({ length: 1000 }, (_, i) => 0.01 * (i + 1));

const naive = prices.reduce((a, b) => a + b, 0);
const precise = Math.sumPrecise(prices);

console.log(naive === precise);  // false, reduce has accumulation error
console.log(precise);            // Correct value

The most classic demonstration is this:

const tricky = [1e20, 0.1, -1e20];

console.log(tricky.reduce((a, b) => a + b, 0));  // 0 ❌ That 0.1 was simply swallowed
console.log(Math.sumPrecise(tricky));             // 0.1 ✅

During normal accumulation, the result of 1e20 + 0.1 is approximately 1e20 (the precision of 0.1 is swallowed by the huge difference in magnitude), and then - 1e20 leaves only 0. The Kahan-Neumaier algorithm records the discarded mantissa at each addition step and compensates for it at the end.

In one sentence: Math.sumPrecise() fixes the error accumulated from "adding many times," not the inherent problem that "0.1 itself is imprecise." If you need arbitrary-precision decimal arithmetic (like bank interest calculations), you still need to use BigInt or a decimal library.

4. Array.fromAsync(): The Destination for Async Iterators

Array.from() is used daily—converting a sync iterator into an array. But the async version with the same name never existed:

// Old way: iterate async iterator, manually push
const results = [];
for await (const item of asyncGenerator()) {
  results.push(await processItem(item));
}

Now there's Array.fromAsync():

// ES2026: Generate an array directly from an async data source
const files = await Array.fromAsync(
  getAsyncFilenames(),
  async (name) => await readFile(name, 'utf-8')
);
// Returns in order, regardless of concurrency

// Also supports sync generators yielding Promises
function* gen() {
  yield fetch('/api/users/1').then(r => r.json());
  yield fetch('/api/users/2').then(r => r.json());
}
const users = await Array.fromAsync(gen());

Essentially, it takes items from an async iterator one by one and puts them into an array to return. The second parameter is an optional mapping function, consistent with the usage of Array.from().

5. Uint8Array Gets Native Base64 / Hex Encoding and Decoding

When the front end deals with binary data, Base64 and hexadecimal conversions are almost unavoidable. Previously, you had to write your own or pull in a library:

// Old methods: btoa / atob, and only supports Latin-1 characters
const text = 'Hello World';
const base64 = btoa(text);  // Fails when encountering Chinese characters

// Hexadecimal conversion required writing a loop yourself
function bytesToHex(bytes) {
  return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
}

ES2026 provides native methods directly on Uint8Array:

// Encoding
const encoder = new TextEncoder();
const data = encoder.encode('Hello, ES2026! 😎');

const base64 = data.toBase64();          // "SGVsbG8sIEVTMjAyNiEg8J+Yjg=="
const hex = data.toHex();                // "48656c6c6f2c204553323032362120f09f988e"

// Also supports URL-safe Base64
const urlSafe = data.toBase64({ alphabet: 'base64url' });

// Decoding is also simple
const fromB64 = Uint8Array.fromBase64(base64);
const fromHex = Uint8Array.fromHex(hex);

console.log(new TextDecoder().decode(fromB64));  // "Hello, ES2026! 😎"

In the future, for scenarios like image uploads, hash value display, and URL encoding, there's no need to npm install base64-js anymore.

6. Iterator.concat(): Iterator Concatenation Without Array Spreading

Often you have several data sources on hand and want to string them together for iteration. The old way was to spread them into an array and then concatenate:

// Old method: Spread all iterators into memory and then merge
const combined = [...dataSet1, ...dataSet2, ...dataSet3];
// If all three datasets have 100,000 records each, memory explodes

Iterator.concat() returns a lazy iterator, not expanding everything at once:

// ES2026: Lazy concatenation, O(1) memory
const i1 = fetchLargeDataset('table1');
const i2 = fetchLargeDataset('table2');
const i3 = ['fallback-item'];

const combined = Iterator.concat(i1, i2, i3);

// Consumed one by one during iteration, won't blow up memory
for (const row of combined) {
  processRow(row);
}

// Can mix in plain values in the middle
const months = Iterator.concat(
  Iterator.from([2022, 2023]),
  [2024],                         // Plain arrays can also be inserted
  Iterator.from([2025, 2026])
);
console.log(Array.from(months));  // [2022, 2023, 2024, 2025, 2026]

This is especially useful for scenarios involving large file streams, paginated data, and multiple API data sources—avoiding the waste of "stuffing several large datasets into memory all at once before processing."

7. Map.getOrInsert(): Eliminate if-has-set-get in One Line

Almost every project has code like this:

const cache = new Map();

// Old way: first check if it exists, if not, insert it, then retrieve it
function getCached(key, computeFn) {
  if (!cache.has(key)) {
    cache.set(key, computeFn());
  }
  return cache.get(key);
}

This four-line boilerplate code appears repeatedly in Map usage scenarios. ES2026 compresses it directly into one beat:

// ES2026: Done in one line
const settings = new Map();
settings.set('language', 'en');

settings.getOrInsert('theme', 'dark');     // 'dark' (doesn't exist, inserts default)
settings.getOrInsert('language', 'pl');    // 'en' (exists, returns original value)

// Classic caching pattern:
const cache = new Map();
const result = cache.getOrInsert(key, () => expensiveComputation());
// Expensive computation only executes when the key doesn't exist

WeakMap also supports this. Stop handwriting that if-has-set-get pattern.

8. Temporal API: The Date Object Finally Has a Savior

Speaking of Date, JS developers can rant for half an hour:

Temporal is a date-time library redesigned from scratch by TC39. First, look at a comparison:

// ❌ Date: Month starts from 0, immutable operations require manual handling
const d = new Date(2026, 5, 27);  // 5 = June, guess if you can
d.setMonth(d.getMonth() + 1);     // Directly modified the original object!

// ✅ Temporal: Clear semantics, immutable, month starts from 1
import { Temporal } from '@js-temporal/polyfill';

const today = Temporal.PlainDate.from({ year: 2026, month: 6, day: 27 });
const nextMonth = today.add({ months: 1 });  // Original today unchanged, returns a new object

console.log(today.toString());       // "2026-06-27"
console.log(nextMonth.toString());   // "2026-07-27"
console.log(today.dayOfWeek);        // 6 (Saturday)

A Few Practical Examples

// Date difference calculation
const start = Temporal.PlainDate.from('2026-01-01');
const end = Temporal.PlainDate.from('2026-07-15');
const diff = end.since(start, { largestUnit: 'month' });
console.log(`Difference is ${diff.months} months and ${diff.days} days`);

// Time zone conversion, no manual calculation needed
const shanghaiTime = Temporal.ZonedDateTime.from({
  timeZone: 'Asia/Shanghai',
  year: 2026, month: 7, day: 15, hour: 20,
});
const nyTime = shanghaiTime.withTimeZone('America/New_York');
console.log(nyTime.toString());
// "2026-07-15T08:00:00-04:00[America/New_York]"

// Calculate the last day of next month
const lastDay = Temporal.PlainDate.from('2026-07-31');
const nextLastDay = lastDay.add({ months: 1 });
// Automatically handles differences in month lengths

Core Type Quick Reference

Type Purpose
PlainDate Pure date, e.g., 2026-07-15
PlainTime Pure time, e.g., 14:30:00
PlainDateTime Date + time, no time zone
ZonedDateTime Complete time with time zone
Instant An absolute moment on the timeline (UTC)
Duration A time period, e.g., 2 hours 30 minutes

Note: Temporal can be used in all environments via the @js-temporal/polyfill, with a completely consistent API and a small footprint. It can gradually replace the basic date operations of dayjs/date-fns in your projects.

9. Pattern Matching: Goodbye to the if-else Pyramid

When handling multiple types of branching logic, the traditional JS approach is really verbose:

// Old way: if-else nesting
function describe(value) {
  if (value === null || value === undefined) {
    return 'Null or undefined';
  } else if (typeof value === 'number') {
    if (value > 0) return `Positive number: ${value}`;
    if (value < 0) return `Negative number: ${value}`;
    return 'Zero';
  } else if (typeof value === 'string') {
    if (value.length > 10) return `Long text: ${value.slice(0, 10)}...`;
    return `String: ${value}`;
  } else if (Array.isArray(value)) {
    if (value.length === 0) return 'Empty array';
    return `Array with ${value.length} items`;
  }
  return 'Other';
}

Pattern Matching makes this scenario much clearer:

// ES2026: match expression
function describe(value) {
  return value match {
    null     => 'Null or undefined',
    n when n > 0  => `Positive number: ${n}`,
    n when n < 0  => `Negative number: ${n}`,
    0        => 'Zero',
    s when typeof s === 'string' && s.length > 10
             => `Long text: ${s.slice(0, 10)}...`,
    s when typeof s === 'string'
             => `String: ${s}`,
    []       => 'Empty array',
    Array a  => `Array with ${a.length} items`,
    _        => 'Other',
  };
}

Nested Destructuring Is Also Natural

// Handling various possible event types
const event = {
  type: 'payment_success',
  payload: {
    orderId: 'ORD-2026-0715',
    amount: 9900,        // Unit: cents
    buyer: { id: 1024, vip: true },
  },
};

const notification = event match {
  { type: 'payment_success', payload: { orderId, amount, buyer: { vip: true } } }
    => `🎉 VIP Order ${orderId}, Paid ¥${(amount / 100).toFixed(2)}`,
  { type: 'payment_success', payload: { orderId, amount } }
    => `✅ Regular Order ${orderId}, Paid ¥${(amount / 100).toFixed(2)}`,
  { type: 'refund', payload: { orderId, reason } }
    => `↩️ Refund: ${orderId}, Reason: ${reason}`,
  { type: 'error', payload: { code } } when code >= 500
    => `🚨 Server Internal Error: ${code}`,
  { type: 'error', payload: { code } }
    => `⚠️ Request Error: ${code}`,
  _
    => 'Unknown event type',
};
// "🎉 VIP Order ORD-2026-0715, Paid ¥99.00"

A few key points:

Note: Pattern Matching is currently a Stage 3 proposal and has not yet entered the formal specification. Syntax details may still be adjusted.

10. Records & Tuples: Native Immutable Data

If you've used React or Redux, you're certainly familiar with "immutable updates." Previously, you either relied on self-discipline (just don't mutate) or used libraries like Immer for help:

// Old method: Shallow copies easily introduce bugs
const state = { user: { name: 'Xiao Ming' }, count: 1 };
const newState = { ...state };
newState.count = 2;

console.log(state.count);  // Still 1 ✅
console.log(newState.count); // 2 ✅

// But nested objects fail
newState.user.name = 'Xiao Hong';
console.log(state.user.name);  // 'Xiao Hong' 😱 The original object was modified!

ES2026 introduces Record (#{}) and Tuple (#[]), which are natively immutable data structures:

// Record: Immutable object
const user = #{
  name: 'Xiao Ming',
  age: 28,
  scores: #[85, 92, 78],  // Tuple: Immutable array
};

// All "modification" operations return new objects
const older = user.with({ age: 29 });     // Original user unchanged
const withCity = user.with({ city: 'Shanghai' });

// Content equality means reference equality
const user2 = #{ name: 'Xiao Ming', age: 28, scores: #[85, 92, 78] };
console.log(user === user2);  // true! ✅

How to Use in React State Management

// Use Record for state, no need for Immer
let state = #{
  users: #[
    #{ id: 1, name: 'Xiao Ming', active: true },
    #{ id: 2, name: 'Xiao Hong', active: false },
  ],
  filter: 'all',
};

// Update a specific user
state = state.with({
  users: state.users.map(u =>
    u.id === 1 ? u.with({ active: !u.active }) : u
  ),
});

// state is a new object, React's shallow compare can correctly detect the change
// Meanwhile, the original data is completely unaffected

Deep content equality comparison is the biggest advantage of Records & Tuples—you no longer need to handwrite isEqual, no need to import lodash/isEqual, just use === directly. This is a huge boon for React's memo, useMemo, and useEffect dependency arrays.

Note: Records & Tuples are also currently a Stage 3 proposal and have not yet entered the formal specification.

Summary

Looking back at these 10 features, you'll find they share a common thread: None of them are for showing off; they are all filling in gaps.

The path JavaScript has walked over these ten years is quite interesting: ES6 gave it a modern skeleton, and ES2026 is starting to fill in the flesh and blood. These features won't make you exclaim "black magic," but one afternoon you'll suddenly realize—hey, didn't this code used to require a dozen lines to write? How come it's done in three lines now.

Comments

Top 2 of 4 from juejin.cn, machine-translated. The original thread is authoritative.

蓝莓Zz

Great!!!

秋天的一阵风

[rose]

鲨蜂啦

The article has depth, the thinking has height [thumbs up][thumbs up][thumbs up]

秋天的一阵风

[rose]