JavaScript, one of the most used programming languages in the world, keeps evolving to make itself more efficient and expressive, but more importantly, to make our lives as developers easier.
This year, it officially received a new set of features: ES2025.
Here are 8 of them, with examples, so you can discover what’s new and maybe bring them into your projects (or not… but hey, it’s always fun to learn something new & shiny).
#1. Promise.try()
Promise.try()
is a great new way to deal with inconsistent functions, sometimes found in older or legacy code that you don’t necessarily have control over. By inconsistent, I mean that they sometimes return a value immediately (synchronously), and other times, they return a promise (asynchronously).
This problem forces you to write messy and defensive code like this:
// Imagine a messy, inconsistent function you have no control over
function getProfile(userId) {
if (userId === 'sync-error') {
throw new Error('No userId provided.')
}
if (userId === 'sync-user') {
return {name: 'John', id: userId },
}
return new Promise((resolve, reject) => {
setTimeout(() => {
if (userId === 'async-error') {
reject(new Error('Server failed to fetch user data'));
} else {
resolve({ name: 'Jane', id: userId });
}
}, 100);
});
}
// Here's how you might deal with that function
try {
const profile = getProfile('sync-user');
if (profile instanceof Promise) {
profile.then(data => {
console.log('Async success:', data);
}).catch(error => {
console.error('Async error:', error.message);
});
} else {
console.log('Sync success:', profile);
}
} catch (error) {
console.error('Sync error:', error.message);
}
That’s kind of confusing and hard to maintain. With the new Promise.try()
, you can refactor it to this:
// That new helper can handle all the errors the messy
// function could throw at you.
Promise.try(() => getProfile('async-error'))
.then(profile => console.log('Success:', profile))
.catch(error => console.error('Error with async-error:', error.message));
With this new feature, you now have one consistent way to handle a function that could either be synchronous or asynchronous, while allowing for the code to be way more readable and maintainable.
#2. Set
Methods
Before these new Set
methods were introduced, performing set operations in JavaScript was quite tedious, very manual, and overly verbose.
Take a look at this example:
const cookingClub = new Set(['Adam', 'Eve', 'Reggie', 'Jane']);
const volunteers = new Set(['Reggie', 'Jane', 'John', 'Bob']);
// Now we want to find out who is in both lists
const commonMembers = [];
cookingClub.forEach(member => {
if (volunteers.has(member)) {
commonMembers.push(member);
}
});
console.log(commonMembers); // ['Reggie', 'Jane']
It works — but that’s a lot of code for something so simple.
With the new methods, it becomes much cleaner and more semantic:
const commonMembers = cookingClub.intersection(volunteers);
console.log(commonMembers); // Set(2) { 'Reggie', 'Jane' }
// or we can find who is in the cooking club but not a volunteer
const differenceMembers = cookingClub.difference(volunteers);
console.log(differenceMembers) // Set(2) { 'Adam', 'Eve' }
No more manual loops. Just one-liners that read exactly like what you’re trying to do.
Other new methods, like union
and symmetricDifference
, will give you even more power. If the terms bring back memories of math class, don’t worry — the MDN docs have a great visual guide that makes them click.
If you ever need to compare sets or arrays, these methods will make your code shorter, clearer, and a lot easier to maintain.
#3. Iterator Helpers
Alright, I’m sure you’re familiar with iterators in JavaScript, but if you’re not, here’s a quick reminder: an iterator is simply an object that has a next()
method, which returns a value in the form of { value: foo, done: false }
.
Now, what about generators? To keep it simple, a generator is a function defined with an asterisk after the function
keyword (function*
) that uses the yield
keyword to return values, one at a time:
function* numberGenerator() {
yield 1;
yield 2;
yield 3;
}
const gen = numberGenerator();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: undefined, done: true }
Now that we’re on the same page, what exactly are “iterator helpers”?
They’re basically the good old array methods we know and love (.map()
, .filter()
, .reduce()
, etc.), but available directly on iterators.
One practical use case I can think of is reading a huge CSV file.
Here’s how you may do it, the old fashioned way:
// A simple CSV reader
function* csvReader(csvString) {
const lines = csvString.split('\\n');
for (const line of lines) {
if (line) yield line.split(',');
}
}
// Simulate millions of lines
const csvData = `id,product,category,price\\n1,apple,fruit,1.00\\n
2,banana,fruit,0.50\\n
3,tomato,vegetable,0.75\\n...millions of lines...`;
// Loads the entire dataset into memory - not a good
// idea, but this is for science!
const allRecords = [...csvReader(csvData)];
let totalPrice = 0;
for (const record of allRecords) {
const [id, product, category, price] = record;
if (category === 'fruit') {
totalPrice += parseFloat(price);
}
}
console.log('Total fruit price:', totalPrice);
Sure, this is a practice example, but scale it up to millions of lines and you’ll quickly run into trouble. Not only do we run into memory issues, but the readability isn’t great either. The new ES2025 iterator helpers give us a much nicer way to solve this:
// A simple CSV reader
function* csvReader(csvString) {
const lines = csvString.split('\\n');
for (const line of lines) {
if (line) yield line.split(',');
}
}
// Simulate millions of lines
const csvData = `id,product,category,price\\n1,apple,fruit,1.00\\n
2,banana,fruit,0.50\\n
3,tomato,vegetable,0.75\\n...millions of lines...`;
const records = csvReader(csvData);
// Use a chainable, lazy pipeline to process the data
const fruitPrices = records
.drop(1) // Drop the header row
.filter(record => record[2] === 'fruit') // Filter for 'fruit' category
.map(record => parseFloat(record[3])); // Convert price to number
// Consume the iterator to calculate the total
let totalPrice = 0;
for (const price of fruitPrices) {
totalPrice += price;
}
console.log('Total fruit price:', totalPrice);
Why this is better
- We don’t put all records into a single array; instead, we keep them as an iterator. That means nothing is processed ahead of time—
map
andfilter
only run on one record at a time as we iterate. - From a readability standpoint, this approach is much clearer: it shows the step-by-step process in a declarative style.
I won’t go through every helper method here — there are a lot — but hopefully this gives you a good idea of how to use them in a real-world scenario.
#4. Float16Array
To keep it short: it’s a new typed array that stores 16-bit floating-point numbers. That’s it.
You can almost skip to the next part, because that’s really the whole story. Before this, JavaScript only had Float32Array
, and now it has a smaller version of floating-point numbers.
I can’t imagine myself using this feature often, but if you’re working with low-level coding, machine learning, or you want to squeeze every bit of performance out of your code, this new typed array might serve you well.
In summary: the use case is memory efficiency—using the smallest data type for the right job.
But I can do you one better by visualizing it:
// Create a Float32Array to store 100,000 numbers
const float32Array = new Float32Array(100000);
// Each number takes 4 bytes (32 bits), so the total size is:
const float32Size = float32Array.byteLength;
console.log(`Float32Array size: ${float32Size} bytes`); // Output: 400000 bytes
// Time for the new type!
// Create a Float16Array to store the same number of elements
const float16Array = new Float16Array(100000);
// Each number takes 2 bytes (16 bits), so the total size is:
const float16Size = float16Array.byteLength;
console.log(`Float16Array size: ${float16Size} bytes`); // Output: 200000 bytes
So yeah, same number of elements, but half the memory.
#5. Import Attributes & JSON Modules
These two features are related, so I think we can bundle them together.
You might have come across syntax like this in a TypeScript file:
import data from "./data.json" assert { type: "json" };
The assert
syntax was intended to provide hints to the runtime, telling it to treat the file as JSON data rather than executable code.
But in ES2025, this syntax is deprecated in favor of a new feature: the with
keyword.
import data from "./data.json" with { type: "json" };
It does the same thing, but with
is now the standard.
Use case:
The primary use case is to load non-JS modules, such as JSON modules and CSS modules.
I think this feature offers a lot: better clarity in the code and improved security, since you explicitly tell the runtime what type of file you’re importing instead of letting it assume anything.
Hands-On Example
<!DOCTYPE html>
<html>
<head>
<title>Playground</title>
</head>
<body>
<div class="playground">
</div>
<script type="module" src="app.js"></script>
</body>
</html>
.playground {
padding: 1rem;
border: 1px solid #ccc;
border-radius: 4px;
background-color: red;
}
{
"name": "Playground"
}
import styles from './foo-style.css' with { type: 'css' };
import data from './data.json' with { type: 'json' };
document.adoptedStyleSheets.push(styles);
document.querySelector('.playground').textContent = data.name;
So we have 4 files:
- index.html → displays the result
- CSS and JSON files → test the new import features
- JavaScript file → the heart of this simple web app
And this is the result:
This might not be a huge change, but it does make imports clearer and helps standardize the process.
⚠️ Disclaimer: The import attribute code above works in Chrome, but I’m not sure how many browsers support it yet. Safari, for example, doesn’t support it at the time of writing.
#6. RegExp.escape()
When users type search terms, they almost always expect a literal match.
For example, if a user enters:
cat*
Most users mean the full word cat*
, not the regex version, i.e. “cat followed by 0 or more t
".
The same goes for something like:
cat.
A user expects to find "cat."
, not “cat followed by any single character”.
So what’s the problem?
In regular expressions, many characters have special meaning instead of being a normal character.
Mentioned examples:
*
→ repeat he previous character 0 or more times.
→ match any single character (except newline)
So if we directly use the user’s input without escaping, we end up with:
const text = 'cat, cot, cut, cat* cat.';
// Example 1: user types 'cat*'
const regex1 = new RegExp('cat*', 'g'); // add 'g' flag so we see all matches
console.log(text.match(regex1));
// 👉 ['cat', 'cat', 'cat']
// ✅ Matches: 'cat', 'cat*', 'cat.'
// Note: the '*' applies to the preceding 't', so the match is just "cat".
// (* repeats 't')
// Example 2: user types 'cat.'
const regex2 = new RegExp('cat.', 'g');
console.log(text.match(regex2));
// 👉 ['cat,', 'cat*', 'cat.']
// ✅ Matches: 'cat,', 'cat*', 'cat.'
// (. matches any single character)
That left us with 2 options.
- Escaping manually
// Escape regex special characters
const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const text = 'cat, cot, cut, cat* cat.';
// User types 'cat*'
const escapedRegex1 = new RegExp(escapeRegex('cat*'));
console.log(text.match(escapedRegex1));
// 👉 Matches only: 'cat*'
// User types 'cat.'
const escapedRegex2 = new RegExp(escapeRegex('cat.'));
console.log(text.match(escapedRegex2));
// 👉 Matches only: 'cat.'
It works, but it’s boilerplate and easy to get wrong.
- The new ES2025
RegExp.escape()
const text = 'cat, cot, cut, cat* cat.';
// User types 'cat*'
const safeRegex1 = new RegExp(RegExp.escape('cat*'));
console.log(text.match(safeRegex1));
// 👉 Matches only: 'cat*'
// User types 'cat.'
const safeRegex2 = new RegExp(RegExp.escape('cat.'));
console.log(text.match(safeRegex2));
// 👉 Matches only: 'cat.'
No more writing your own escape logic — the engine does it for you.
I’d say this addition is a good one. It removes the need for manual escaping and ensures that user input is always treated as a literal string.
#7. RegExp Pattern Modifiers (Inline Flags)
Another RegExp QoL update from ES2025. This feature lets you apply a flag to a specific part of a regular expression. It’s really useful when you want different parts of your pattern to behave in different ways, instead of relying on a global modifier.
Let’s start with the syntax below:
(?i:...)
→ turn on case-insensitive matching just for this group(?-i:...)
→ turn off case-insensitive matching just for this group
This also works for other flags like i
, m
, and s
.
// (?i:...) - Activates case-insensitivity
const regex1 = /(?i:hello)/;
regex1.test("Hello"); // true
// (?-i:...) - Deactivates case-insensitivity
const regex2 = /^(?-i:hello)$/i; // The global 'i' is active, but is turned off for 'hello'
regex2.test("hello"); // true
regex2.test("HELLO"); // false
Quick note: Other modifiers are not supported, since they would either make your regex overly complicated or simply wouldn’t make sense when applied locally instead of globally.
Let’s understand it better with an example.
Imagine you need to match a file path that starts with a case-sensitive drive letter (C:
), followed by a case-insensitive directory name (Users
), and then a specific case-sensitive filename (MyFile.txt
).
Old way
// This regex makes the entire expression case-insensitive.
const regex = /C:\\Users\\MyFile\.txt/i;
console.log(regex.test("c:\\users\\myfile.txt"));
// Output: true (too broad - it should fail!)
See the problem? This regex matches both c:
and C:
, which might not be the behavior we want. And just thinking about building a complicated regex to cover all these cases is enough to make me want to take a coffee break.
New way
// The (?i:...) applies the case-insensitive flag only to "users"
const regex = /C:\\(?i:Users)\\MyFile\.txt/;
console.log(regex.test("C:\\users\\MyFile.txt")); // Output: true (correct)
console.log(regex.test("C:\\Users\\MyFile.txt")); // Output: true (correct)
console.log(regex.test("c:\\users\\MyFile.txt")); // Output: false (correct)
console.log(regex.test("C:\\users\\myfile.txt")); // Output: false (correct)
This addition is pretty elegant, as it allows you to write precise regex that match exactly what you want, without needing complicated workarounds.
#8. Duplicate Named Capture Groups
Traditionally, every named capture group in a JavaScript regex had to be unique. If you tried to reuse a name, it would throw a SyntaxError
. This often led to repetitive names like year1
or year2
, even when they represented the same concept.
Well, that restriction has been lifted: you can now reuse the same name in a single regex, and the results will be returned as an array of matches under that name.
// New ES2025 behavior
const regex = /(?<num>\d+)-(?<num>\d+)-(?<num>\d+)/;
const result = regex.exec("123-456-789");
console.log(result.groups.num);
// Output: ["123", "456", "789"]
Example:
Old way
const str = "Today is 2025-09 and tomorrow is 09-2025";
// Had to use two different names: year1 and year2
const regex = /(?<year1>[0-9]{4})-[0-9]{2}|[0-9]{2}-(?<year2>[0-9]{4})/g;
const result = [...str.matchAll(regex)];
// Collect whichever group matched
const years = result.map(r => r.groups.year1 || r.groups.year2);
console.log(years);
// Output: ["2025", "2025"]
New way
const str = "Today is 2025-09 and tomorrow is 09-2025";
// Same group name reused across both alternations
const regex = /(?<year>[0-9]{4})-[0-9]{2}|[0-9]{2}-(?<year>[0-9]{4})/g;
const result = [...str.matchAll(regex)];
console.log(result.map(r => r.groups.year));
// Output: ["2025", "2025"]
This is much cleaner, with no need for names like year1
and year2
Note: This feature is still new and not yet widely available in all JavaScript environments, so please be cautious when using it in production code.
Wrapping Up
And that’s a tour of the shiny new features in ES2025!
Some of these updates are small quality-of-life helpers — like RegExp.escape()
and inline pattern modifiers — while others open up brand-new possibilities, like iterator helpers or Float16Array
for squeezing more performance out of your code.
The important thing isn’t memorizing every single detail, but knowing what tools exist so you can reach for them when the time is right. Even if you don’t need Promise.try()
or duplicate capture groups today, just being aware they’re available can save you hours of work down the road.
JavaScript continues to evolve in ways that make it more expressive, more efficient, and frankly more enjoyable to write. Try out these features in your own projects, keep an eye on browser and runtime support, and have fun exploring what’s new.
Because at the end of the day: better tools mean less boilerplate, cleaner code, and more time to build the stuff you actually care about.