Upcoming JavaScript features in 2026

Published on
Updated on

JS logo 2026

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 'https://waspdev.com/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:

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:

The Temporal API has a separation of concepts and introduces the following main classes and concepts:

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.






Disqus uses cookies, please check Privacy & cookies before loading the comments.
Please enable JavaScript to view the comments powered by Disqus.


UP
This site uses cookies for some services. By clicking Accept, you agree to their use. To find out more, including how to control cookies, see here: Privacy & cookies.