Upcoming JavaScript features in 2026
Published on
Updated on

It's 2026. JavaScript continues to gain new features and improvements. Below I'll list the most important features that we'll hopefully see in 2026.
Map upsert
Have you ever been in a situation where you had to update some value in
Map
but were not certain whether a key existed or not? For example, you need to count the number of occurrences of
each character in a string.
You would probably do something like this:
const string = "counting characters in a string";
const map = new Map();
for (const character of string) {
let counterObj = map.get(character);
if (!counterObj) {
counterObj = {
character,
count: 0,
};
map.set(character, counterObj);
}
counterObj.count++;
}
console.log(map);
Alternatively, you could also use map.has(character), but handling
missing keys still requires additional lookups (including set()) in the map. In any case, writing
this is a bit bulky, also the additional lookups add some performance overhead (even if they happen only
once per unique key).
Fortunately, this will become a thing of the past.
Map.prototype.getOrInsert()
and
Map.prototype.getOrInsertComputed()
allow to add the missing map entry with only one lookup. WeakMap also has the same methods:
WeakMap.prototype.getOrInsert()
and
WeakMap.prototype.getOrInsertComputed().
So, the same thing could be done like this:
const string = "counting characters in a string";
const map = new Map();
for (const character of string) {
// This is a combination of get and set. When it
// fails to find the key, it inserts
// the value provided in the 2-nd parameter and returns it
const counterObj = map.getOrInsert(character, {
character,
count: 0,
});
counterObj.count++;
}
console.log(map);
However, if the construction of the value in the 2-nd parameter is expensive, you can use
getOrInsertComputed() instead:
const string = "counting characters in a string";
const map = new Map();
for (const character of string) {
// In this case a callback is provided, the callback will be called
// only when the key is missing, so the default
// value construction will be avoided when it's not necessary
const counterObj = map.getOrInsertComputed(character, () => ({
character,
count: 0,
}));
counterObj.count++;
}
console.log(map);
The feature is not yet supported in all major browsers. So, don't rely on this in production code.
Lossless JSON serialization and deserialization
In JavaScript it's impossible to encode and decode some values as JSON. For example, it's not possible to encode
and decode bigint numbers:
const obj = {
prop: 1n,
};
console.log(JSON.stringify(obj)); // Exception
One workaround for this is to convert bigint numbers into strings by passing the replacer
callback to
JSON.stringify():
const obj = {
prop: 1n,
};
console.log(
JSON.stringify(obj, (key, value) => typeof value === "bigint" ? value.toString() : value)
); // {"prop":"1"}
When parsing the JSON with
JSON.parse()
we can pass the reviver
callback where we can convert the stringified bigint back to bigint for known
properties:
console.log(JSON.parse(
'{"prop":"138","otherProp":"789"}',
(key, value) => key === "prop" && typeof value === "string" ? BigInt(value) : value
)); // { prop: 138n, otherProp: "789" }
It works, but it's complicated for parsing when dealing with deep objects. You have to identify the property
values
that indeed need to be converted into bigint.
A better solution is to encode bigint into a regular number and convert it back. This is possible
because the JSON standard itself has no precision / digit count limit for both integers and floating point
numbers.
Now, we need to have some control and access to the raw JSON property value. And we can achieve this, thanks to
JSON.rawJSON()
and
context.source
of JSON.parse() reviver callback.
JSON.rawJSON() accepts the original form of the stringified primitive value. It
returns a special exotic object. When that object is stringified the stringified value is preserved
(there is no wrapping in quotes or any major transformation). That
stringified value is directly injected into JSON.
Keep in mind that JSON.rawJSON() only accepts
valid stringified primitive values and will throw error if non valid value is passed.
context.source holds the original JSON string in the raw form.
So, by combining JSON.rawJSON() and context.source we can encode and decode some
values easily. Here is an example with bigintnumbers:
const obj = {
regularNumber: 99999,
bigRegularNumber: 999999999999999999,
bigInt: 999999999999999999n,
};
const serializedJSON = JSON.stringify(
obj,
(key, value) => typeof value === "bigint" ? JSON.rawJSON(value.toString()) : value,
);
console.log(serializedJSON);
// '{"regularNumber":99999,"bigRegularNumber":1000000000000000000,"bigInt":999999999999999999}'
// Notice that bigRegularNumber had a precision loss because the value
// (999999999999999999) is outside the safe integer range.
// Meanwhile, bigInt was encoded into regular JSON number without losses.
console.log(JSON.parse(serializedJSON));
// {regularNumber: 99999, bigRegularNumber: 1000000000000000000, bigInt: 1000000000000000000}
// If the number value is not a safe integer and the raw JSON source is in a form of integer
// we can decode it back as bigint
console.log(JSON.parse(
serializedJSON,
(key, value, { source }) =>
typeof value === 'number' && !Number.isSafeInteger(value) && /^-?[0-9]+$/.test(source) ?
BigInt(source) : value),
);
// {regularNumber: 99999, bigRegularNumber: 1000000000000000000n, bigInt: 999999999999999999n}
JSON.rawJSON()
is not yet supported in Safari. So, don't rely on this in production code.
Iterator.concat()
A nice handy feature. As the name suggests,
Iterator.concat()
does concatenation of iterable objects. It works very similarly to
Array.prototype.concat(),
just supports all types of iterables and accepts only iterable objects. The return value is an iterator which is
lazily evaluated. Here is an example of using Iterator.concat():
function* generate() {
yield 1;
yield 2;
yield 3;
}
const iterable = Iterator.concat(generate(), [4, 5, 6])
console.log(iterable); // Iterator
console.log(iterable.toArray()); // [ 1, 2, 3, 4, 5, 6 ]
Iterator.concat({}, 1); // exception, all arguments must be iterable
The feature has a very limited support. So, don't rely on this in production code.
Math.sumPrecise()
Another nice but somewhat niche feature is
Math.sumPrecise().
Math.sumPrecise() receives an iterable of numbers and sums them more precisely compared to a
naive loop based sum. It uses a specialized summing algorithm. This feature might be useful when doing some
financial or math calculations.
Here is an example of Math.sumPrecise() vs regular sum when calculating the approximate value of
the mathematical constant
e using the power series
formula:
// The power series for approximation of mathematical constant e
function* sequenceOfE() {
let member = 1;
yield member;
for (let i = 1; i < 20; ++i) {
member /= i;
yield member;
}
}
const regularSum = sequenceOfE().reduce((acc, cur) => acc + cur, 0);
const preciseSum = Math.sumPrecise(sequenceOfE());
console.log(regularSum, preciseSum); // 2.7182818284590455 2.718281828459045
// Calculating the actual errors compared to Math.E
console.log(Math.abs(Math.E - regularSum), Math.abs(Math.E - preciseSum));
// 4.440892098500626e-16 0
// Math.sumPrecise() can reduce the error
The feature has a very limited support. So, don't rely on this in production code.
import defer (proposal)
import defer is a
proposal of lazy module evaluation. JS modules can get very large, their initialization can be
really expensive. Yes, theoretically lazy loading can be done via dynamic
import().
However, this
results in all functions and their callers switching to an asynchronous programming model. As a result, all
callers must be updated to accommodate the
new model, which is impossible without introducing API changes that break compatibility with existing API
consumers. Doesn't
this remind you of anything?
In order to solve this, a new syntax was introduced - import defer. The idea is when the module is
imported via import defer, it isn't evaluated immediately. So, no CPU blocking occurs immediately.
The
module has to be evaluated only when
the script is accessing it for the first time. But note that, the execution will still be paused until the
module is downloaded. The code that uses
deferred modules looks like a normal code, and
doesn't need
significant changes:
import defer * as heavyModule from '/static/js/test/some-script.js';
// The file '/static/some-script.js'will not be evaluated immediately
// and won't block the CPU until it's used. Although the script execution will be
// paused until the resource is completely downloaded.
console.log('heavyModule is not evaluated yet');
// will print as soon as the module is fetched, since the import is not blocking the CPU
setTimeout(() => {
// When heavyScript is about to be accessed for the first time,
// the execution is paused until the module is evaluated.
// After that heavyScript can be safely used, like a normal module.
// The code that uses a deferred module looks like a normal code.
heavyModule.someFunction();
}, 1000);
This feature has reached stage 3. Hopefully, browsers will implement this soon.
Error.isError()
Error.isError()
allows to determine whether the value is a genuine error or not. It's also kinda possible to do this via
value instanceof Error. However, it's not very robust, since this can be faked
by patching the prototype. Error.isError() actually checks a private internal field which is
impossible to
fake. It's very similar to
Array.isArray().
Here are some examples of Error.isError() vs instanceof Error:
const genuineError1 = new Error();
const genuineError2 = new TypeError();
const genuineFalseNegativeError = new TypeError();
Object.setPrototypeOf(genuineFalseNegativeError, Object.prototype);
const fakeFalsePositiveError = Object.create(TypeError.prototype);
console.log(
genuineError1 instanceof Error,
genuineError2 instanceof Error,
genuineFalseNegativeError instanceof Error,
fakeFalsePositiveError instanceof Error,
); // true true false true
console.log(
Error.isError(genuineError1),
Error.isError(genuineError2),
Error.isError(genuineFalseNegativeError),
Error.isError(fakeFalsePositiveError),
); // true true true false
The feature has almost
universal support. Safari still returns false for DOMException.
Uint8Array.fromBase64()
Uint8Array.fromBase64()
allows to create Uint8Array from a base 64-encoded string. A nice handy feature to have:
const bytes = new Uint8Array([0, 15, 16, 127, 255]);
const b64 = bytes.toBase64();
console.log(b64); // "AA8Qf/8="
const roundTrip = Uint8Array.fromBase64(b64);
console.log(roundTrip.toHex()); // "000f107fff"
This is feature is already supported by the latest major browsers. So, you can aleready use it with caution.
Element.setHTML() and Sanitizer API
Element.setHTML()
and
Sanitizer API
allow to parse an HTML string safely and sanitize it in order to prevent Cross Site Scripting (XSS) attacks.
This is a long awaited feature, since many websites, especially content management systems, heavily rely on
dynamic text content, such as saved rich
texts, comments, posts, etc. Often such content is saved in a form of raw HTML which can be directly
embedded into the web page html. So, this creates potential security issues if the website doesn't do any
validations on the HTML text. Some common security problems are:
- Inline scripts, which can contain potentially malicious code. For example:
<script> alert("Potentially malicious code"); </script> -
Inline styles, which don't execute code by themselves but can still modify or break the
website UI. For example:
<style> body { background: red; } </style> -
Dangerous HTML tags and attributes. This is also related to the previous 2 points
to some extent. Some HTML elements, especially when used with certain attributes can also have dangerous
behaviors. For example:
<a href="javascript:alert('Potentially malicious link');">Potentially malicious link</a> <button onclick="alert('Potentially malicious button');">Potentially malicious button</button> <img src="some_invalid_url" onerror="alert('Potentially malicious error callback');">
Element.setHTML() is safe compared to
Element.innerHTML.
Here is an example of Element.setHTML():
const el = document.createElement('div');
el.setHTML(`
<script>
alert("Potentially malicious code");
</script>
<style>
body {
background: red;
}
</style>
<a href="javascript:alert('Potentially malicious link');">Potentially malicious link</a>
<button onclick="alert('Potentially malicious button');">Potentially malicious button</button>
<img src="some_invalid_url" onerror="alert('Potentially malicious error callback');">
`); // By default removes all unsafe elements and attributes
console.log(el.innerHTML);
Sanitizer API can be used with Element.setHTML() to customize the sanitization:
const unsafeHTML = `
<script>
alert("Potentially malicious code");
</script>
<style>
body {
background: red;
}
</style>
<a href="javascript:alert('Potentially malicious link');">Potentially malicious link</a>
<button onclick="alert('Potentially malicious button');">Potentially malicious button</button>
<img src="some_invalid_url" onerror="alert('Potentially malicious error callback');">
`;
const sanitizer = new Sanitizer();
const el = document.createElement('div');
el.setHTML(unsafeHTML, { sanitizer });
// By default the sanitizer removes all unsafe elements and attributes
console.log(el.innerHTML);
sanitizer.allowElement("img"); // Allow <img> elements
sanitizer.allowAttribute("src"); // Allow [src] attribute
sanitizer.removeElement("a"); // Disallow <a> elements
el.setHTML(unsafeHTML, { sanitizer }); // This time <a> will be removed and <img>
// with [src] attribute will be present
console.log(el.innerHTML);
Both
Sanitizer API an Element.setHTML() are not yet supported in Safari. So,
don't rely on this in production code.
Temporal API
Historically the old Date
object has been a pain to work with. It has many quirks, missing features and footguns. Temporal API fixes
many of these problems.
Over the years, developers identified numerous limitations, inconsistencies, and bugs in the Date
API. Many of
these stem from the API's original design and have proven very difficult to fix without breaking the web.
The Temporal API is designed as a comprehensive replacement for Date, tackling its deficiencies
head-on. It
introduces a suite of new immutable date/time objects and utility methods that cover a wide range of use cases.
Some of the key goals and improvements of Temporal include:
- First-class time zone support.
- Immutability.
- Consistent parsing and ISO 8601 support.
- Finer precision and extended range.
The Temporal API has a separation of concepts and introduces the following main classes and concepts:
-
Temporal.PlainDate. A calendar date with no time or time zone attached. -
Temporal.PlainTime. A wall-clock time with no associated date or time zone. -
Temporal.PlainMonthDay. A month and day of a calendar date, without a year or time zone. -
Temporal.PlainYearMonth. A year and month of a calendar date, without a day (no day-of-month) or time zone. -
Temporal.PlainDateTime. A date and time (together) without a time zone. -
Temporal.ZonedDateTime. A date and time with an associated time zone. -
Temporal.Instant. A specific point on the global timeline -
Temporal.Duration. A time span or difference between two temporal points. - Calendar systems.
Here is an example of converting Tokyo time to London time by using Temporal.ZonedDateTime:
// Create date/time in Tokyo time zone:
const tokyoTime = Temporal.ZonedDateTime.from({
timeZone: "Asia/Tokyo",
year: 2025,
month: 5,
day: 24,
hour: 10,
minute: 0,
second: 0,
});
console.log(tokyoTime.toString());
// "2025-05-24T10:00:00+09:00[Asia/Tokyo]"
// Convert the same instant to London time:
const sameLondonTime = tokyoTime.withTimeZone("Europe/London");
console.log(sameLondonTime.toString());
// "2025-05-24T02:00:00+01:00[Europe/London]"
console.log(tokyoTime.toInstant().toString());
console.log(sameLondonTime.toInstant().toString());
// Both will output "2025-05-24T01:00:00Z" as ISO string
// since their underlying Instant is the same
For more details and other use cases, you can read this post about
Temporal API.
Temporal API has limited browser support. So,
don't rely on this in production code.
Explicit resource management
Explicit resource management adds a nice and clean syntax to automatically release the resources when their references are becoming unavailable. This often happens when you open a file or network socket.
For example, in the past, to reliably accomplish this task, experienced developers would typically write something like this:
let fileHandle;
try {
fileHandle = await fs.open('some_file_path', 'r');
// Do something
// ...
} finally {
// Wrapping in finally guarantees that the
// file handle will eventually close
// even in a case of an error.
fileHandle?.close();
}
With Explicit resource management such things can be done more cleanly.
Suppose you have some resource class called DisposableResource and you want the cleanup method to
be
called automatically when the resource variable is no longer in the block scope. With the help of keyword
using
and symbol
Symbol.dispose
you can do like this:
class DisposableResource {
constructor() {
console.log('The resource is created');
}
[Symbol.dispose]() {
console.log('The resource is disposed');
}
}
{
// Keyword "using" behaves like "const", but also tells the compiler to dispose
// the object immediately after the declared variable becomes unreachable from the scope.
using resource = new DisposableResource();
console.log('Doing something with the resource...');
}
// After the end of the resource block scope [Symbol.dispose]() is called.
And what's more, [Symbol.dispose]() will be called even when an uncaught error is thrown:
class DisposableResource {
constructor() {
console.log('The resource is created');
}
[Symbol.dispose]() {
console.log('The resource is disposed');
}
}
{
using resource = new DisposableResource();
throw "Error";
}
// [Symbol.dispose]() will be called even when an uncaught error occurs.
For more details and other use cases, you can read this post about Explicit
resource management.
Explicit
resource management has limited browser support. So,
don't rely on this in production code.
Conclusion
Year 2026 brings several nice features that we hope will be implemented soon. These new features will make JavaScript more robust and powerful, allowing developers to build complex applications with greater confidence and fewer frustrations.