What's new in ECMAScript 2025
Another version of ECMAScript version has been approved by the TC39, and to keep my annual tradition I’m sharing what’s new in the ES2025 with simple practical examples. If you want to catch up with the previous editions, here you have them: 2024, 2023, 2022, 2021, 2020, 2019, 2018, 2017 and 2016. Now, let’s see what is new this year.
- Duplicate named capturing groups
- Set Methods for JavaScript
- Regular Expression Pattern Modifiers
- Import Attributes
- Iterator Helpers
- Promise.try()
- Float16Array
- RegExp Escaping
Duplicate named capturing groups
Regex capture groups are super helpful, but before ES2025 it was disallowed to duplicate them across alternatives (chunks separated by |). Prior to this release, it resulted in a syntax error, but this years update makes it work.
const pattern = /ECMAScript(?<version>[0-9]{4})|ES(?<version>[0-9]{2})/;
// 👆 👆
// <ES2025
"ECMAScript2025".match(pattern);
// SyntaxError: Invalid regular expression: /ECMAScript(?<version>[0-9]{4})|ES(?<version>[0-9]{2})/: Duplicate capture group name
// >=ES2025
"ECMAScript2025".match(pattern);
// "2025"
Set Methods for JavaScript
This proposal makes Sets more powerful. It adds the following methods to the Set prototype: intersection, union, difference, symmetricDifference, isSubsetOf, isSupersetOf, and isDisjointFrom.
const setOne = new Set([1, 2, 3]);
const setTwo = new Set([3, 4, 5]);
setOne.intersection(setTwo);
// Set(1) { 3 }
setOne.union(setTwo);
// Set(5) { 1, 2, 3, 4, 5 }
setOne.difference(setTwo);
// Set(2) { 1, 2 }
setOne.symmetricDifference(setTwo);
// Set(4) { 1, 2, 4, 5 }
setOne.isSubsetOf(setTwo);
// false
setOne.isSupersetOf(setTwo);
// false
setOne.isDisjointFrom(setTwo);
// false
MDN makes a great job of explaining every single one of these methods, but to be fully proficient with Sets, I would highly recommend catching up on some basics of the Set theory.
Regular Expression Pattern Modifiers
This one allows developers to add regular expression flags (i for case insensitivity, m for multi-line and so on) to a subset of patterns in contrast to the whole expression. Look at this example that targets the word “bearer” and ignores capitalisation, followed by the literal “abc” that needs to be lowercase. Subexpression modifiers are present in Regex engines built into other programming languages like Pearl or .NET, and this year are also available in ECMAScript. This is super useful!
const pattern = /^(?i:bearer) abc$/;
pattern.test("bearer abc");
// true
pattern.test("Bearer abc");
// true
pattern.test("bEaReR abc");
// true
pattern.test("bearer ABC");
// false
Import Attributes and JSON modules
We have been importing non-JS files for ages. Thanks to the powerful bundlers, importing a CSS, SVG file, or JSON data heap has become the norm. The new import type attribute informs the module system about the MIME type, configures fetching behaviour, and orchestrates parsing and evaluation of the imported asset. This ensures that the correct validation rules are applied and prevents potential security risks. Implementing this feature was a basis for JSON modules, and as of now this is the only non-JS format supported, but additional types will come in the following years.
// Import
import data from "./data.json" with { type: "json" };
// Dynamic import
const data = await import("./data.json", { with: { type: "json" } });
// Re-export
export { data } from "./data.json" with { type: "json" };
Iterator Helpers
I love working with iterators in Rust. This proposal brings us closer to this realm and makes working with them in JavaScript a lot more enjoyable. No more Array.from() or some third party libs. This is by far my favourite proposal that landed in the new specification. Here is a list of new methods!
map()filter()take()drop()flatMap()reduce()toArray()forEach()some()every()find()Iterator.from()(static method)
Some of them (map, filter, reduce, flatMap, some, find and every) mimic the naming and functionality of methods from the Array.prototype, and their usecase and functionality should be familiar. Here is a quick example.
const iter = [..."ECMAScript2025"].values();
const iterNumeric = iter.filter((c) => /^\d$/.test(c));
iterNumeric.next();
// { value: '2', done: false }
iterNumeric.next();
// { value: '0', done: false }
iterNumeric.next();
// { value: '2', done: false }
iterNumeric.next();
// { value: '5', done: false }
iterNumeric.next();
// { value: undefined, done: true }
Some of them (drop, take) are just for iterator manipulations and are not present in an Array.prototype.
const iter = [..."ECMAScript2025"].values();
const iterNumeric = iter.drop(10).take(4);
iterNumeric.next();
// { value: '2', done: false }
iterNumeric.next();
// { value: '0', done: false }
iterNumeric.next();
// { value: '2', done: false }
iterNumeric.next();
// { value: '5', done: false }
iterNumeric.next();
// { value: undefined, done: true }
Promise.try()
Wrapping a non-sync function in a Promise is a common operation. The new Promise.try is an elegant wrapper for sync or async operations that ensures a returned promise. Here is a simple use case.
const handleAction = (action) =>
action
.then((result) => console.log(result))
.catch((error) => console.error(error))
.finally(() => console.log("done"));
handleAction(new Promise((resolve, reject) => resolve("all good")));
// all good
// done
const handleAction = (action) =>
action
.then((result) => console.log(result))
.catch((error) => console.error(error))
.finally(() => console.log("done"));
handleAction(new Promise((resolve, reject) => reject("uuuupps")));
// uuuupps
// done
The handleAction works great with these two, because in both cases the passed argument is a Promise. What will happen if the argument is a sync operation though?
const handleAction = (action) =>
action
.then((result) => console.log(result))
.catch((error) => console.error(error))
.finally(() => console.log("done"));
handleAction(() => "look ma, no promise!");
// TypeError: action.then is not a function
This is a common situation when people reach for npm i p-try, but now that’s not needed as this helper is built into the language. Here is an example with a sync action again, and another one when it throws.
const handleAction = (action) =>
Promise.try(action) // 👈👈👈
.then((result) => console.log(result))
.catch((error) => console.error(error))
.finally(() => console.log("done"));
handleAction(() => "look ma, no promise!");
// look ma, no promise!
// done
const handleAction = (action) =>
Promise.try(action) // 👈👈👈
.then((result) => console.log(result))
.catch((error) => console.error(error))
.finally(() => console.log("done"));
handleAction(() => {
throw "look me, no promise, but an error!";
});
// look me, no promise, but an error!
// done
Float16Array
To complement the already existing Float32Array and Float64Array in the language, this proposal adds the missing half-precision floating-point format, Float16Array. This proposal hardly makes any difference for web developers, but for graphics-intensive calculations where memory is limited, this can save the day.
RegExp Escaping
Mechanism for escaping literal strings is something that the community has been asking for a very long time. It is present in Perl, PHP, Python, Ruby and many others. Let’s look at the problem and the solution that the new RegExp.escape() solves.
const sentence = "He has two dogs. I have one dog.";
const pattern = /dog./;
const newSentence = sentence.replace(pattern, "cat.");
console.log(newSentence);
// He has two cat.. I have one dog.
Uuups. The . token in the pattern is matching any single character, but our intention was to match a literal .. Here is where the new RegExp.escape() comes in handy.
const sentence = "He has two dogs. I have one dog.";
const pattern = new RegExp(RegExp.escape("dog.")); // 👈👈👈
const newSentence = sentence.replace(pattern, "cat.");
console.log(newSentence);
// He has two dogs. I have one cat.
This example is not the best, but imagine if the value inside the regex is coming from an external source or user input. This solves a huge problem and simplifies otherwise complex and error-prone solutions.
JavaScript Trademark
Did you know that “JavaScript” is a registered trademark and belongs to Oracle? This company has very little to do with this language, but this is the main reason why we refer to this language as ECMAScript, and the reason why organising a “JavaScript Conf” violates trademark rules. Pretty silly, right?
Folks from Deno filed and open petition to release the “JavaScript” trademark. Thousands of developers signed a letter to help to make JavaScript free. If you haven’t already, please do so at javascript.tm. Thank you 😘
Thanks a lot for the recap!
My pleasure 🤗
The term "try" for Promise.try is not ideal IMO. I personally think "Promise.wrap" would have been a better term, because that is what it seems to do.
Going further, in Ruby, there is a "Array(item)" call which returns the item in an array, unless the item is an array, and then just returns it. So perhaps even "Promise(fn)" in JS would have been more ideal than "Promise.try".
Too late to change now ^_^
Hmmm, it does not always wrap. It tries to wrapping only if it needs to, when the asynchronous function is passed, it doesn't do anything. At this is the way how I created this mnemonic in my brain. The idea of
Promise(fn)looks super clean, but unfortunately this is not possible as Promise already works as a constructorPromise(resolve, reject)to this signature wouldn't work.Great stuff, thanks.
Minor typo (no judgment, we all make them) “baseis”
Thanks a ton Yorick. All fixed 😉