DEV Community

Cover image for 7 New JavaScript Features in ECMAScript 2026 That Replace Everyday Workarounds

7 New JavaScript Features in ECMAScript 2026 That Replace Everyday Workarounds

JavaScript receives a new specification every year, but not every release changes the way we write everyday application code.

ECMAScript 2026 is different.

The 17th edition of the ECMAScript specification was approved by Ecma International on June 30, 2026. It introduces several small but highly practical APIs that replace patterns many of us have been writing manually for years.

No new framework is required. No architectural rewrite is necessary. These are language-level improvements for frontend applications, Node.js services, libraries, APIs, data pipelines, and developer tools.

In this article, we will explore seven additions:

  1. Array.fromAsync()
  2. Map.prototype.getOrInsert()
  3. Iterator.concat()
  4. Error.isError()
  5. Math.sumPrecise()
  6. Native Base64 and hex support for Uint8Array
  7. JSON source text access and raw JSON

Let us look at the problems they solve and where they can be useful in real projects.

Before using a new API in production, check support in your target browsers and JavaScript runtimes. Feature detection, progressive enhancement, or a compatible fallback may still be appropriate.


1. Convert async data into an array with Array.fromAsync()

JavaScript has supported Array.from() for synchronous iterables for years.

const values = Array.from(new Set([1, 2, 2, 3]));

console.log(values);
// [1, 2, 3]
Enter fullscreen mode Exit fullscreen mode

However, Array.from() cannot consume an asynchronous iterable.

Until now, collecting values from an async generator usually required a manual loop:

async function* loadPages() {
  yield await fetchPage(1);
  yield await fetchPage(2);
  yield await fetchPage(3);
}

const pages = [];

for await (const page of loadPages()) {
  pages.push(page);
}
Enter fullscreen mode Exit fullscreen mode

With ECMAScript 2026, we can express the same intention more directly:

const pages = await Array.fromAsync(loadPages());
Enter fullscreen mode Exit fullscreen mode

Array.fromAsync() can also apply a mapping function while collecting the values:

const titles = await Array.fromAsync(
  loadPages(),
  page => page.title
);
Enter fullscreen mode Exit fullscreen mode

A simplified full-stack example could collect results from a paginated API:

async function* fetchAllUsers() {
  let page = 1;

  while (true) {
    const response = await fetch(`/api/users?page=${page}`);
    const result = await response.json();

    for (const user of result.users) {
      yield user;
    }

    if (!result.nextPage) {
      return;
    }

    page = result.nextPage;
  }
}

const activeUsers = await Array.fromAsync(
  fetchAllUsers(),
  user => ({
    id: user.id,
    name: user.name,
    active: user.lastSeenAt !== null
  })
);
Enter fullscreen mode Exit fullscreen mode

Why it matters

This is useful when working with:

  • paginated API responses,
  • database cursors,
  • asynchronous generators,
  • streamed records,
  • file-processing pipelines,
  • values that resolve asynchronously.

The main benefit is readability. The code clearly says: consume this asynchronous source and return an array.

One important consideration

An array keeps every collected item in memory. If the source can produce a very large or unlimited number of values, process items incrementally with for await...of instead.


2. Initialize Map values with getOrInsert()

A common Map pattern is to check whether a value exists, create it if necessary, and then retrieve it.

For example, grouping products by category often looks like this:

const productsByCategory = new Map();

for (const product of products) {
  if (!productsByCategory.has(product.category)) {
    productsByCategory.set(product.category, []);
  }

  productsByCategory.get(product.category).push(product);
}
Enter fullscreen mode Exit fullscreen mode

ECMAScript 2026 adds Map.prototype.getOrInsert():

const productsByCategory = new Map();

for (const product of products) {
  productsByCategory
    .getOrInsert(product.category, [])
    .push(product);
}
Enter fullscreen mode Exit fullscreen mode

If the key exists, the method returns the current value. If it does not exist, the provided default is inserted and returned.

There is also getOrInsertComputed(), which calculates the value only when a key is missing:

const userCache = new Map();

function getUserProfile(userId) {
  return userCache.getOrInsertComputed(
    userId,
    () => createUserProfile(userId)
  );
}
Enter fullscreen mode Exit fullscreen mode

This is especially useful when creating the default value is expensive.

const reportCache = new Map();

function getReport(projectId) {
  return reportCache.getOrInsertComputed(projectId, () => ({
    projectId,
    createdAt: new Date(),
    rows: buildInitialReportRows(projectId)
  }));
}
Enter fullscreen mode Exit fullscreen mode

Equivalent methods are also available for WeakMap.

Why it matters

These methods make several common patterns more concise:

  • grouping API results,
  • memoization,
  • per-request caches,
  • dependency containers,
  • graph construction,
  • counters and indexes,
  • metadata associated with objects.

They also communicate intent better than separate has(), set(), and get() calls.


3. Combine lazy sequences with Iterator.concat()

Suppose an application receives items from multiple sources:

function* localArticles() {
  yield { id: 1, source: "local" };
  yield { id: 2, source: "local" };
}

function* partnerArticles() {
  yield { id: 3, source: "partner" };
  yield { id: 4, source: "partner" };
}
Enter fullscreen mode Exit fullscreen mode

Previously, we might have created an array:

const articles = [
  ...localArticles(),
  ...partnerArticles()
];
Enter fullscreen mode Exit fullscreen mode

This immediately consumes both iterators and stores all their values in memory.

ECMAScript 2026 provides Iterator.concat():

const articles = Iterator.concat(
  localArticles(),
  partnerArticles()
);

for (const article of articles) {
  console.log(article);
}
Enter fullscreen mode Exit fullscreen mode

The combined iterator remains lazy. Values are requested only when the consumer needs them.

It can also combine arrays and other synchronous iterables:

const navigationItems = Iterator.concat(
  ["Home", "Articles"],
  getProjectLinks(),
  ["Settings"]
);
Enter fullscreen mode Exit fullscreen mode

Why laziness is useful

Lazy iteration can help when:

  • inputs are large,
  • producing an item requires computation,
  • the consumer may stop early,
  • allocating an intermediate array is unnecessary.

For example:

const allCandidates = Iterator.concat(
  getPrimaryCandidates(),
  getFallbackCandidates(),
  getArchivedCandidates()
);

for (const candidate of allCandidates) {
  if (candidate.matches(query)) {
    console.log("Found:", candidate);
    break;
  }
}
Enter fullscreen mode Exit fullscreen mode

The later sources may never be evaluated if an earlier match is found.


4. Detect genuine Error objects with Error.isError()

Most JavaScript developers use instanceof to determine whether a caught value is an error:

try {
  await saveDocument();
} catch (value) {
  if (value instanceof Error) {
    console.error(value.message);
  }
}
Enter fullscreen mode Exit fullscreen mode

This works in many applications, but it can be unreliable when the error comes from a different JavaScript realm, such as:

  • an iframe,
  • another browser window,
  • certain VM environments,
  • isolated execution contexts.

Each realm can have a different Error constructor. An error created elsewhere may therefore fail the local instanceof Error check.

ECMAScript 2026 introduces Error.isError():

try {
  await saveDocument();
} catch (value) {
  if (Error.isError(value)) {
    console.error(value.message);
  } else {
    console.error("A non-Error value was thrown:", value);
  }
}
Enter fullscreen mode Exit fullscreen mode

A reusable normalization helper becomes straightforward:

function normalizeError(value) {
  if (Error.isError(value)) {
    return value;
  }

  if (typeof value === "string") {
    return new Error(value);
  }

  return new Error("An unknown error occurred", {
    cause: value
  });
}
Enter fullscreen mode Exit fullscreen mode

Why it matters

This API is particularly useful for:

  • component libraries,
  • iframe-based applications,
  • plugin systems,
  • test runners,
  • browser automation,
  • sandboxed code,
  • error-monitoring SDKs.

It gives the language a direct and explicit way to answer a surprisingly difficult question: is this value actually an Error object?


5. Sum floating-point values more accurately with Math.sumPrecise()

Floating-point arithmetic can lose precision, especially when values with very different magnitudes are added sequentially.

Consider this calculation:

const values = [1e16, 1, -1e16];

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

console.log(total);
// Commonly 0, although the mathematical result is 1
Enter fullscreen mode Exit fullscreen mode

The small 1 can disappear during intermediate floating-point operations.

ECMAScript 2026 adds Math.sumPrecise():

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

console.log(total);
// 1
Enter fullscreen mode Exit fullscreen mode

It accepts an iterable, so it can also work with data transformations:

const completedOrderValues = orders
  .filter(order => order.status === "completed")
  .map(order => order.total);

const revenue = Math.sumPrecise(completedOrderValues);
Enter fullscreen mode Exit fullscreen mode

Or with a generator:

function* validMeasurements(rows) {
  for (const row of rows) {
    if (Number.isFinite(row.measurement)) {
      yield row.measurement;
    }
  }
}

const total = Math.sumPrecise(
  validMeasurements(dataset)
);
Enter fullscreen mode Exit fullscreen mode

Important: this is not decimal arithmetic

Math.sumPrecise() reduces floating-point accumulation error, but JavaScript numbers are still IEEE 754 floating-point values.

For money, the safest approach is usually to store minor units as integers:

const pricesInCents = [1999, 2500, 349];

const totalInCents = pricesInCents.reduce(
  (sum, value) => sum + value,
  0
);
Enter fullscreen mode Exit fullscreen mode

Use Math.sumPrecise() when you intentionally work with floating-point measurements, statistics, coordinates, scientific data, or mixed-magnitude values.


6. Encode and decode binary data without custom Base64 helpers

Web applications frequently need to convert binary data to Base64 or hexadecimal strings.

Common use cases include:

  • API payloads,
  • cryptographic data,
  • file chunks,
  • content hashes,
  • authentication challenges,
  • browser storage.

Historically, developers often combined btoa(), atob(), loops, buffers, or third-party helpers. Those approaches could be awkward, environment-specific, or unsafe for arbitrary binary data.

ECMAScript 2026 adds dedicated methods to Uint8Array.

Base64

const bytes = new Uint8Array([
  72, 101, 108, 108, 111
]);

const encoded = bytes.toBase64();

console.log(encoded);
// SGVsbG8=
Enter fullscreen mode Exit fullscreen mode

Decode it again:

const decoded = Uint8Array.fromBase64(
  "SGVsbG8="
);

console.log(decoded);
// Uint8Array(5) [72, 101, 108, 108, 111]
Enter fullscreen mode Exit fullscreen mode

Hexadecimal

const token = new Uint8Array([
  15, 160, 255
]);

console.log(token.toHex());
// 0fa0ff
Enter fullscreen mode Exit fullscreen mode

And in the opposite direction:

const bytes = Uint8Array.fromHex(
  "0fa0ff"
);
Enter fullscreen mode Exit fullscreen mode

A digest helper can now be written cleanly:

async function createSha256Hex(input) {
  const source = new TextEncoder().encode(input);

  const digest = await crypto.subtle.digest(
    "SHA-256",
    source
  );

  return new Uint8Array(digest).toHex();
}
Enter fullscreen mode Exit fullscreen mode

Why it matters

These methods create a clearer, standardized path for binary conversion across JavaScript environments.

The code is easier to review because developers no longer need to recognize a custom chain of character conversions as β€œbinary data to Base64.”


7. Preserve important JSON source information

JSON numbers are parsed into JavaScript Number values. This can cause information loss when the source contains an integer larger than JavaScript can represent safely.

const payload =
  '{"transactionId":123456789012345678901234567890}';

const result = JSON.parse(payload);

console.log(result.transactionId);
// The original integer may already have lost precision
Enter fullscreen mode Exit fullscreen mode

ECMAScript 2026 extends the JSON.parse() reviver with access to the original source text.

const payload =
  '{"transactionId":123456789012345678901234567890}';

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

    return value;
  }
);

console.log(result.transactionId);
// 123456789012345678901234567890n
Enter fullscreen mode Exit fullscreen mode

The key detail is context.source. It shows the matched JSON source before the already-parsed Number value becomes the only available representation.

This is useful for:

  • database identifiers,
  • blockchain values,
  • high-precision API data,
  • large counters,
  • scientific datasets,
  • interoperability with systems using 64-bit integers.

ECMAScript 2026 also includes JSON.rawJSON(), which lets code provide validated raw JSON for primitive values during serialization.

const exactId = JSON.rawJSON(
  "123456789012345678901234567890"
);

const payload = JSON.stringify({
  transactionId: exactId
});

console.log(payload);
// {"transactionId":123456789012345678901234567890}
Enter fullscreen mode Exit fullscreen mode

This offers more control over output that cannot be represented exactly as a regular JavaScript Number.

Use it carefully

Raw JSON is a precision and interoperability tool, not a general string-concatenation shortcut.

Only use validated values and keep the expected schema explicit.


A quick migration checklist

You do not need to refactor an entire codebase to benefit from ES2026.

Look for these existing patterns:

Manual async collection

const results = [];

for await (const item of source) {
  results.push(item);
}
Enter fullscreen mode Exit fullscreen mode

Consider:

const results = await Array.fromAsync(source);
Enter fullscreen mode Exit fullscreen mode

Repeated Map initialization

if (!map.has(key)) {
  map.set(key, createValue());
}

const value = map.get(key);
Enter fullscreen mode Exit fullscreen mode

Consider:

const value = map.getOrInsertComputed(
  key,
  createValue
);
Enter fullscreen mode Exit fullscreen mode

Array spreads used only to join iterables

const combined = [
  ...sourceA,
  ...sourceB
];
Enter fullscreen mode Exit fullscreen mode

Consider a lazy sequence:

const combined = Iterator.concat(
  sourceA,
  sourceB
);
Enter fullscreen mode Exit fullscreen mode

Cross-context error checks

value instanceof Error
Enter fullscreen mode Exit fullscreen mode

Consider:

Error.isError(value)
Enter fullscreen mode Exit fullscreen mode

Custom binary conversion helpers

customBytesToHex(bytes)
Enter fullscreen mode Exit fullscreen mode

Consider:

bytes.toHex()
Enter fullscreen mode Exit fullscreen mode

Large integers parsed from JSON

If an API returns integer values outside the safe Number range, consider parsing the original source into BigInt or a decimal representation.


How to use these features safely today

A feature being part of the ECMAScript standard does not guarantee that every browser, embedded webview, server runtime, or build target already supports it.

A simple feature check can help:

const supportsFromAsync =
  typeof Array.fromAsync === "function";

const supportsPreciseSum =
  typeof Math.sumPrecise === "function";

const supportsErrorCheck =
  typeof Error.isError === "function";

const supportsHex =
  typeof Uint8Array.prototype.toHex === "function";
Enter fullscreen mode Exit fullscreen mode

For a production project:

  1. Check your browser and runtime support matrix.
  2. Test using the same versions as your deployment environment.
  3. Add a fallback or polyfill where appropriate.
  4. Avoid assuming that transpilation automatically provides new runtime APIs.
  5. Upgrade dependencies and CI images deliberately.

Transpilers can transform syntax, but methods such as Math.sumPrecise() and Array.fromAsync() require runtime support or an implementation supplied by your application.


Final thoughts

ECMAScript 2026 is a good example of language evolution focused on practical improvements rather than dramatic syntax changes.

Its additions address familiar problems:

  • collecting asynchronous data,
  • initializing cached values,
  • combining lazy sequences,
  • identifying errors reliably,
  • reducing summation error,
  • converting binary data,
  • preserving JSON precision.

None of these features will replace your framework or redesign your architecture. That is exactly why they are valuable: they make ordinary JavaScript code shorter, clearer, and more expressive.

My first candidates for immediate adoption are:

  1. Map.prototype.getOrInsertComputed() for caches and grouping,
  2. Array.fromAsync() for finite asynchronous sources,
  3. the new Uint8Array encoding methods for API and crypto work.

Which ECMAScript 2026 feature are you most likely to use first?


Sources and further reading


Connect with Me

If you found this guide helpful, let's connect and discuss modern development workflows!


This article was created with the help of AI

Top comments (0)