← Back to the blogECMAScript 2026: seven new JavaScript APIs with practical examples
javascriptecmascriptnodejsweb-developmenttc39

ECMAScript 2026: seven new JavaScript APIs with practical examples

ECMAScript 2026 introduces improvements for Maps, Iterators, errors, bytes, numeric operations, asynchronous sources, and JSON without precision loss. Several already work in current runtimes, but support is not yet consistent.

ECMAScript 2026 introduces improvements for Maps, Iterators, errors, bytes, numeric operations, asynchronous sources, and JSON without precision loss. Several already work in current runtimes, but support is not yet consistent.

The 17th edition of ECMA-262 does not introduce syntax that completely changes how we write JavaScript. Instead, it standardizes seven small solutions to problems we have spent years solving with helpers, libraries, or repetitive code.


ECMAScript 2026 is the 17th edition of the language standard. Its official additions are:

  1. Map and WeakMap with getOrInsert.
  2. Access to the original text in JSON.parse and JSON.rawJSON.
  3. Iterator.concat.
  4. Native Base64 and hexadecimal support in Uint8Array.
  5. Math.sumPrecise.
  6. Error.isError.
  7. Array.fromAsync.

In this article, we will look at the problem each API solves, how to use it, and what support it actually has.

Map of the seven ECMAScript 2026 APIs

Before we begin: ES2026 is not the same as “everything new in JavaScript”

Some lists published online mix features from different editions.

These APIs belong to ECMAScript 2025, not 2026:

  • RegExp.escape.
  • Promise.try.
  • Iterator Helpers.
  • New Set methods.
  • Import Attributes and JSON Modules.

The following are not part of ES2026 either:

  • Temporal.
  • Explicit Resource Management and the using syntax.

Both proposals reached Stage 4 after the edition cutoff and are planned for ECMAScript 2027.

1. Map.getOrInsert: get a value or create it

Grouping items in a Map usually requires two lookups:

const groups = new Map();

for (const item of items) {
  if (!groups.has(item.category)) {
    groups.set(item.category, []);
  }

  groups.get(item.category).push(item);
}

ECMAScript 2026 adds:

Map.prototype.getOrInsert
Map.prototype.getOrInsertComputed
WeakMap.prototype.getOrInsert
WeakMap.prototype.getOrInsertComputed

The previous example becomes:

const groups = new Map();

for (const item of items) {
  groups
    .getOrInsert(item.category, [])
    .push(item);
}

If the key exists, it returns the current value. If it does not, it inserts the default value and returns it.

Compute the value only when needed

There is no reason to construct an expensive object if the key already exists:

const cache = new Map();

const profile = cache.getOrInsertComputed(
  userId,
  key => loadProfile(key)
);

The getOrInsertComputed callback runs only when the key is not yet in the Map.

It also receives the key as an argument:

const groups = new Map();

const frontend = groups.getOrInsertComputed(
  'frontend',
  key => [key]
);

console.log(frontend);
// ['frontend']

This API is similar to Java's computeIfAbsent, C#'s GetOrAdd, and Python's setdefault.

2. Array.fromAsync: convert an asynchronous source into an array

Until now, converting an async iterable required manually accumulating its values:

const values = [];

for await (const value of asyncSource) {
  values.push(value);
}

With ES2026:

const values = await Array.fromAsync(asyncSource);

It also accepts a mapping function:

async function* pages() {
  yield 1;
  yield Promise.resolve(2);
}

const values = await Array.fromAsync(
  pages(),
  value => value * 10
);

console.log(values);
// [10, 20]

It is not the same as Promise.all

Array.fromAsync consumes the source progressively and awaits its values sequentially, following for await behavior.

const sequential = await Array.fromAsync(promises);

Promise.all first iterates over the entire source and awaits the results in parallel:

const parallel = await Promise.all(
  Array.from(promises)
);

The right option depends on whether you need lazy iteration, memory control, or parallelism.

3. Iterator.concat: combine iterables without creating intermediate arrays

To sequence multiple iterables, we used to write a generator:

function* concat(...iterables) {
  for (const iterable of iterables) {
    yield* iterable;
  }
}

ECMAScript 2026 adds Iterator.concat:

const values = Iterator.concat(
  [1, 2],
  new Set([3, 4]),
  [5]
);

console.log([...values]);
// [1, 2, 3, 4, 5]

The operation is lazy. Calling the method does not create an array containing all the elements.

This is useful for:

  • Combining paginated results.
  • Chaining multiple sources.
  • Adding prefixes or suffixes to an Iterator.
  • Processing large sequences without fully materializing them.
const records = Iterator.concat(
  cachedRecords,
  databaseRecords,
  generatedRecords
);

for (const record of records) {
  process(record);
}

4. Base64 and hexadecimal directly in Uint8Array

JavaScript had atob and btoa, but those APIs work with binary strings, not byte arrays.

ES2026 adds:

Uint8Array.fromBase64()
Uint8Array.fromHex()

Uint8Array.prototype.toBase64()
Uint8Array.prototype.toHex()
Uint8Array.prototype.setFromBase64()
Uint8Array.prototype.setFromHex()

Encode bytes

const bytes = new TextEncoder().encode('Hello');

console.log(bytes.toBase64());
// SGVsbG8=

console.log(bytes.toHex());
// 48656c6c6f

Decode Base64

const bytes = Uint8Array.fromBase64('SGVsbG8=');
const text = new TextDecoder().decode(bytes);

console.log(text);
// Hello

URL-safe Base64

const token = bytes.toBase64({
  alphabet: 'base64url',
  omitPadding: true,
});

This variant is common in JWTs, URLs, and tokens.

Write into an existing buffer

setFromBase64 avoids creating another Uint8Array:

const target = new Uint8Array(8);

const result = target.setFromBase64('Zm9vYmFy');

console.log(result);
// { read: 8, written: 6 }

5. Error.isError: detect genuine errors

The usual check is:

value instanceof Error

The problem appears when the error comes from another realm, such as an iframe, a worker, or a context created with node:vm.

const iframe = document.createElement('iframe');
document.body.append(iframe);

const externalError =
  new iframe.contentWindow.Error('boom');

console.log(externalError instanceof Error);
// false

ES2026 adds a check equivalent to Array.isArray:

console.log(Error.isError(externalError));
// true

It also avoids relying on:

Object.prototype.toString.call(value)

That technique can be fooled through Symbol.toStringTag.

Error.isError is useful in:

  • Centralized logging systems.
  • Exception serialization.
  • Applications with iframes or workers.
  • Code that may receive any value through throw.
try {
  await execute();
} catch (value) {
  if (Error.isError(value)) {
    logger.error(value.message, value.stack);
  } else {
    logger.error('Unknown thrown value', value);
  }
}

6. Math.sumPrecise: add numbers with less accumulated error

Sequential floating-point addition can lose small values:

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

const total = values.reduce(
  (sum, value) => sum + value,
  0
);

console.log(total);
// 0

The mathematical result is 0.1, but the value is lost because of the order of operations.

ECMAScript 2026 adds:

const total = Math.sumPrecise([
  1e20,
  0.1,
  -1e20,
]);

console.log(total);
// 0.1

The method takes an iterable, not separate arguments:

Math.sumPrecise(values);

This avoids the stack limits that a variadic API would encounter with thousands of elements.

What it does not solve

Math.sumPrecise still works with number values.

It does not turn JavaScript into an exact decimal system and should not be used as a replacement for a decimal library in financial calculations.

It does not accept BigInt either.

Its goal is to return the correctly rounded sum we would obtain by adding with arbitrary precision and converting the result back to floating point.

7. JSON without precision loss

An API may return a perfectly valid integer that does not fit within JavaScript's safe integer range:

{
  "user_id": 9223372036854775807
}

The JSON preserves every digit, but JSON.parse converts the value to a number:

const payload =
  '{"user_id":9223372036854775807}';

const result = JSON.parse(payload);

console.log(result.user_id);
// 9223372036854776000

Converting that result to BigInt does not recover the lost information.

Access the original text

The JSON.parse reviver now receives a third argument:

Lossless JSON flow

const result = JSON.parse(
  payload,
  (key, value, context) => {
    if (key === 'user_id') {
      return BigInt(context.source);
    }

    return value;
  }
);

console.log(result.user_id);
// 9223372036854775807n

context.source contains the exact JSON fragment that produced the primitive value.

A reusable parser

export function parseLosslessJson(text) {
  return JSON.parse(
    text,
    (_key, value, context) => {
      const isUnsafeInteger =
        typeof value === 'number' &&
        !Number.isSafeInteger(value) &&
        /^-?\d+$/.test(context.source);

      return isUnsafeInteger
        ? BigInt(context.source)
        : value;
    }
  );
}

Safe integers and decimals remain number values:

const result = parseLosslessJson(
  '{"user_id":9223372036854775807,"count":42,"ratio":0.125}'
);

console.log(result);
// {
//   user_id: 9223372036854775807n,
//   count: 42,
//   ratio: 0.125
// }

Serialize with JSON.rawJSON

JSON.stringify does not accept BigInt directly:

JSON.stringify({
  user_id: 9223372036854775807n
});

// TypeError

JSON.rawJSON lets you insert validated JSON text:

const encoded = JSON.stringify({
  user_id: JSON.rawJSON(
    '9223372036854775807'
  )
});

console.log(encoded);
// {"user_id":9223372036854775807}

A generic serializer:

export function stringifyLosslessJson(value) {
  return JSON.stringify(
    value,
    (_key, current) =>
      typeof current === 'bigint'
        ? JSON.rawJSON(current.toString())
        : current
  );
}

If you control the API contract, identifiers should still be sent as strings. These new APIs are especially useful when consuming an external contract that you cannot modify.

Actual support in current runtimes

An API's presence in the specification does not mean it is available in every environment.

These checks were performed directly on the listed runtimes on July 26, 2026:

ECMAScript 2026 runtime support matrix

API Node 22.23 Node 24.18 Node 26.5 Chromium 150
Array.fromAsync Yes Yes Yes Yes
Map.getOrInsert No No Yes Yes
Iterator.concat No No Yes Yes
Base64/hex in Uint8Array No No Yes Yes
Error.isError No Yes Yes Yes
Math.sumPrecise No No No Yes
JSON source / raw JSON Yes Yes Yes Yes

The Math.sumPrecise case demonstrates why support should not be assumed based solely on the Node version number: even Node 26 does not implement it yet.

Feature detection

For code that runs across different runtimes:

const support = {
  arrayFromAsync:
    typeof Array.fromAsync === 'function',

  mapGetOrInsert:
    typeof Map.prototype.getOrInsert ===
    'function',

  iteratorConcat:
    typeof Iterator !== 'undefined' &&
    typeof Iterator.concat === 'function',

  uint8ArrayBase64:
    typeof Uint8Array.prototype.toBase64 ===
    'function',

  mathSumPrecise:
    typeof Math.sumPrecise === 'function',

  errorIsError:
    typeof Error.isError === 'function',

  rawJson:
    typeof JSON.rawJSON === 'function',
};

Do not use a fallback that silently changes the result. For critical data, fail explicitly or load a proven polyfill.

Which ones I would use today

In projects where the minimum runtime version is under your control:

  • Map.getOrInsert removes a fair amount of repetitive code.
  • The Uint8Array conversions replace Base64 and hex helpers.
  • Error.isError is a direct improvement for logging.
  • Array.fromAsync simplifies tests and stream consumption.
  • The JSON APIs solve a data integrity problem.

I would use Iterator.concat when lazy evaluation provides a real benefit. For small arrays, spread syntax remains more familiar:

const combined = [...first, ...second];

I would wait before depending on Math.sumPrecise in cross-platform code. Its support is still more limited than the rest of the edition.

Conclusion

ECMAScript 2026 is an edition focused on eliminating small friction points.

It does not change the language's core syntax, but it does reduce the need to maintain custom utilities for:

  • Initializing values in Maps.
  • Consuming asynchronous iterables.
  • Sequencing Iterators.
  • Encoding bytes.
  • Detecting errors across realms.
  • Adding values with less precision loss.
  • Preserving large numbers when processing JSON.

The most important part is not memorizing seven new names. It is recognizing which of these APIs replaces real code in your project and checking whether the runtime where you deploy already implements it.

Sources

Comments

Loading comments…