JavaScript and TypeScript course Β· Module 5: Advanced JavaScript
ES2024/ES2025 Features
In this lesson5
Dr. Henry Wu has just come back from a developer conference. "JavaScript keeps evolving," he says. "The latest ECMAScript versions bring features that will greatly simplify our code!" Until now, grouping required reduce() and an empty object, and sorting a list without destroying the original required a manual copy. The new versions of the standard turn such patches into a single line.
How ECMAScript Evolves
The language is developed by the TC39 committee, and since 2015 a new version of the standard has been released every year, named after the year of publication. A proposal goes through successive stages and enters the standard once it reaches stage 4, which requires, among other things, working implementations. Milestones of this evolution:
- ES6 (ES2015) -
letandconst, arrow functions, destructuring - ES2017 -
asyncandawait - ES2020 - optional chaining (
?.) and nullish coalescing (??) - ES2023 - non-mutating array methods, e.g.
toSorted() - ES2024 -
Object.groupBy()andPromise.withResolvers() - ES2025 - new
Setmethods,Promise.try()and JSON imports
ES2024 - Key New Features
1. Object.groupBy() and Map.groupBy()
Grouping array elements by a key - with no external libraries! You pass an array and a function that returns the name of the group for each dinosaur:
1const dinosaurs = [
2 { name: 'Rex', type: 'carnivore', sector: 7 },
3 { name: 'Blue', type: 'carnivore', sector: 3 },
4 { name: 'Sarah', type: 'herbivore', sector: 5 },
5 { name: 'Bronty', type: 'herbivore', sector: 5 },
6 { name: 'Delta', type: 'carnivore', sector: 3 }
7];
8
9// Grouping by type
10const byType = Object.groupBy(dinosaurs, dino => dino.type);
11console.log(byType);
12// {
13// carnivore: [{ name: 'Rex', ... }, { name: 'Blue', ... }, { name: 'Delta', ... }],
14// herbivore: [{ name: 'Sarah', ... }, { name: 'Bronty', ... }]
15// }
16
17// Grouping by sector
18const bySector = Object.groupBy(dinosaurs, dino => `Sector-${dino.sector}`);
19console.log(bySector);
20
21// Map.groupBy - when you need a Map instead of an object
22const byTypeMap = Map.groupBy(dinosaurs, dino => dino.type);
23console.log(byTypeMap.get('carnivore'));Each key of the result leads to an array of matching dinosaurs - the same thing we built earlier with reduce(). The object returned by Object.groupBy() has no prototype, so it does not inherit methods such as hasOwnProperty(). Choose Map.groupBy() when the keys should be objects rather than strings.
2. Non-Mutating Array Methods (ES2023)
Formally, these methods entered the standard a year earlier, in ES2023, but they are still among the newest tools. They return a new array instead of modifying the original:
1const sectors = ['Sector-1', 'Sector-7', 'Sector-3', 'Sector-5'];
2
3// The old methods MUTATE the original array
4const oldWay = [...sectors];
5oldWay.sort(); // Changes oldWay!
6
7// toSorted() - sorts without mutation
8const sorted = sectors.toSorted();
9console.log(sorted); // ['Sector-1', 'Sector-3', 'Sector-5', 'Sector-7']
10console.log(sectors); // ['Sector-1', 'Sector-7', 'Sector-3', 'Sector-5'] - unchanged!
11
12// toReversed() - reverses without mutation
13const reversed = sectors.toReversed();
14console.log(reversed); // ['Sector-5', 'Sector-3', 'Sector-7', 'Sector-1']
15console.log(sectors); // Still the original!
16
17// toSpliced() - splice without mutation
18const withNew = sectors.toSpliced(1, 1, 'Sector-9', 'Sector-10');
19console.log(withNew); // ['Sector-1', 'Sector-9', 'Sector-10', 'Sector-3', 'Sector-5']
20
21// with() - replace an element without mutation
22const updated = sectors.with(1, 'Sector-ALPHA');
23console.log(updated); // ['Sector-1', 'Sector-ALPHA', 'Sector-3', 'Sector-5']
24
25// Practical example: sorting dinosaurs
26const dinoList = [
27 { name: 'Rex', dangerLevel: 10 },
28 { name: 'Blue', dangerLevel: 8 },
29 { name: 'Sarah', dangerLevel: 3 }
30];
31
32// Sort by danger level (descending) without mutation
33const byDanger = dinoList.toSorted((a, b) => b.dangerLevel - a.dangerLevel);
34console.log(byDanger[0].name); // 'Rex'Each new method has an older, mutating sibling: toSorted() is sort(), toReversed() is reverse(), and toSpliced() is splice(). with(index, value) replaces assignment by index and throws a RangeError for an index outside the array. After all these operations the sectors list looks exactly the same as at the start.
3. Promise.withResolvers()
Easier creation of a Promise with external resolve/reject. It comes in handy when a promise has to be settled from a different place than its constructor:
1// Old way - awkward
2let externalResolve, externalReject;
3const oldPromise = new Promise((resolve, reject) => {
4 externalResolve = resolve;
5 externalReject = reject;
6});
7
8// New way with Promise.withResolvers()
9const { promise, resolve, reject } = Promise.withResolvers();
10
11// Now we can resolve/reject from anywhere
12setTimeout(() => {
13 resolve('Operation complete!');
14}, 1000);
15
16promise.then(console.log); // "Operation complete!" after 1 secondThe old way needs variables declared before the constructor, while Promise.withResolvers() immediately returns an object with three fields, which we unpack with destructuring. It shows best with a sensor that promises a reading now and delivers it later:
1// Useful for creating controllable async operations
2class DinosaurSensor {
3 constructor(id) {
4 this.id = id;
5 const { promise, resolve, reject } = Promise.withResolvers();
6 this.readingPromise = promise;
7 this.resolveReading = resolve;
8 this.rejectReading = reject;
9 }
10
11 receiveReading(data) {
12 this.resolveReading(data);
13 }
14
15 sensorFailure(error) {
16 this.rejectReading(error);
17 }
18}
19
20const sensor = new DinosaurSensor("TREX-001");
21
22// Set up what happens when reading arrives
23sensor.readingPromise.then(reading => {
24 console.log(`Sensor ${sensor.id}: ${JSON.stringify(reading)}`);
25});
26
27// Later, when the reading arrives:
28sensor.receiveReading({ temperature: 38.5, heartRate: 72 });The constructor creates the promise and puts resolve and reject aside for later. The code waiting for the reading subscribes through then(), and receiveReading() fulfills the promise only when the data arrives.
ES2025 - The Newest Features
1. New Set Methods
Set operations - finally built into JavaScript! Each method takes a second set and returns a new Set or a boolean:
1const carnivores = new Set(['Rex', 'Blue', 'Delta', 'Echo']);
2const sectorA = new Set(['Rex', 'Sarah', 'Delta']);
3
4// intersection() - elements in both sets
5const dangerousInA = carnivores.intersection(sectorA);
6console.log([...dangerousInA]); // ['Rex', 'Delta']
7
8// difference() - elements only in the first set
9const safeInA = sectorA.difference(carnivores);
10console.log([...safeInA]); // ['Sarah']
11
12// union() - all elements from both sets
13const allKnown = carnivores.union(sectorA);
14console.log([...allKnown]); // ['Rex', 'Blue', 'Delta', 'Echo', 'Sarah']
15
16// symmetricDifference() - elements in only one of the sets
17const exclusive = carnivores.symmetricDifference(sectorA);
18console.log([...exclusive]); // ['Blue', 'Echo', 'Sarah']
19
20// isSubsetOf() - is it a subset?
21const smallGroup = new Set(['Rex', 'Blue']);
22console.log(smallGroup.isSubsetOf(carnivores)); // true
23
24// isSupersetOf() - does it contain all the elements?
25console.log(carnivores.isSupersetOf(smallGroup)); // true
26
27// isDisjointFrom() - no elements in common?
28const herbivores = new Set(['Sarah', 'Bronty', 'Tricy']);
29console.log(carnivores.isDisjointFrom(herbivores)); // trueThe first four methods return a new Set, and the original sets stay untouched. The methods starting with is answer only true or false, and spread turns the result into an array, because a Set is iterable.
Speaking of sets: the data structure decides the speed. Access by index (array[0]) or to an object property (obj.key) has a constant cost, written as O(1), independent of the number of elements. indexOf() and includes() scan the whole array in the worst case, which is O(n), and sorting is usually O(n log n). Set.has() works in constant time in practice, so when you often check whether something is present, a set beats an array.
2. Promise.try()
Safe execution of a function in a Promise context. A function can fail in two ways: throw an error right away or return a rejected promise, and without Promise.try() both paths have to be handled separately:
1// Without Promise.try - must handle sync/async differently
2function getDinosaurStatus(id) {
3 return new Promise((resolve, reject) => {
4 try {
5 const result = syncOperation(id); // might throw synchronously
6 resolve(asyncOperation(result)); // might reject
7 } catch (e) {
8 reject(e);
9 }
10 });
11}
12
13// With Promise.try - cleaner
14function getDinosaurStatus(id) {
15 return Promise.try(() => {
16 const result = syncOperation(id); // sync throws are caught
17 return asyncOperation(result); // async rejections are caught
18 });
19}
20
21// Practical: safely execute any function as a promise
22const safeCheck = Promise.try(() => {
23 validateDinoId("RAPTOR-001"); // might throw
24 return fetchDinosaurData("RAPTOR-001"); // returns promise
25});
26
27safeCheck
28 .then(data => console.log("Data:", data))
29 .catch(err => console.error("Error:", err));Promise.try() turns an error thrown synchronously into a rejected promise, so a single .catch() catches both kinds of failure. A quick test on a function that throws on demand:
1function riskyOperation(shouldThrow) {
2 if (shouldThrow) {
3 throw new Error('Synchronous error!');
4 }
5 return 'Success';
6}
7
8// New way with Promise.try()
9const result = Promise.try(() => riskyOperation(false));
10result.then(console.log); // 'Success'
11
12const error = Promise.try(() => riskyOperation(true));
13error.catch(err => console.error(err.message)); // 'Synchronous error!'Both results end up in ordinary then() and catch() handlers - the synchronous error did not stop the program, it simply became a rejection.
3. Import Attributes (JSON Modules)
Importing JSON directly. The with { type: 'json' } attribute tells the environment what type of file we are loading:
1// Importing a JSON file with a type attribute
2import dinoConfig from './dinosaurs.json' with { type: 'json' };
3
4console.log(dinoConfig.species);
5
6// Dynamic import with attributes
7const config = await import('./config.json', { with: { type: 'json' } });The attribute is a safeguard: if the server returns a script instead of JSON, the import fails instead of running foreign code. The syntax works in ES modules, which you met earlier in this location.
Checking Browser Support
New features do not reach all browsers at the same time. Before you use them, check that they exist by comparing the type with the string 'function':
1const es2024Support = {
2 objectGroupBy: typeof Object.groupBy === 'function',
3 arrayToSorted: typeof [].toSorted === 'function', // formally ES2023
4 arrayToReversed: typeof [].toReversed === 'function', // formally ES2023
5 arrayWith: typeof [].with === 'function', // formally ES2023
6 promiseWithResolvers: typeof Promise.withResolvers === 'function'
7};
8
9const es2025Support = {
10 setIntersection: typeof Set.prototype.intersection === 'function',
11 setDifference: typeof Set.prototype.difference === 'function',
12 promiseTry: typeof Promise.try === 'function'
13};
14
15console.log('ES2024 Support:', es2024Support);
16console.log('ES2025 Support:', es2025Support);This kind of feature detection is more reliable than guessing by browser name. When a feature is missing, include a polyfill, a script that adds the missing method.
Summary
ES2024 and ES2025 bring:
- Object.groupBy() / Map.groupBy() - clean array grouping without lodash or reduce()
- toSorted(), toReversed(), toSpliced(), with() - immutable array operations without the spread operator (formally since ES2023)
- Promise.withResolvers() - easier Promise management
- Set methods - set operations built into the language
- Promise.try() - uniform handling of synchronous and asynchronous errors
- Import Attributes - an explicit declaration of the imported module's type
Remember to check browser compatibility and use polyfills when needed! My advice: reach for toSorted() and Object.groupBy() freely, but check the compatibility table on MDN when you target older browsers. In the next lesson you will meet the observers built into the browser.
Remember: JavaScript evolves like species in the lab - new features arrive every year, but before releasing them into the enclosure, check that the browser recognizes them.
Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What does the Object.groupBy() method introduced in ES2024 do?
2. How does toSorted() differ from sort() in JavaScript?
These are 2 of 3 questions for this lesson. Solve the rest in the game.
Hands-on tasks in the game
- Code editor
Group dinosaurs by type, sort by threat level, and replace elements without mutation.
- Vertical ordering
Order JavaScript features chronologically by ECMAScript standard: