Some features that every JavaScript developer should know in 2025 (continued)

Published on

Problem description

Some people told me that I didn't list enough features in this article. So I decided to share with you some few other JS features that everyone should know in 2025.

OK, let's begin.

Promise.try()

JavaScript has recently introduced Promise.try(). Promise.try() takes a function and optional list of arguments, then safely executes the function. It returns a promise, if the function returns successfully the promise is resolved with the returned value, otherwise the promise is rejected with the uncaught exception. The passed function might run synchronously and asynchronously, it doesn't matter, the result will always be a promise.

function callback(result, mustThrow) { if (mustThrow) { throw result; } return result; } const promise1 = Promise.try(callback, 'some result', false); const promise2 = Promise.try(callback, 'some error', true); // won't throw error because it's wrapped in the promise promise1.then((value) => console.log(value)); // some result promise2.catch((value) => console.log(value)); // some error async function asyncCallback(result, mustThrow) { return callback(result, mustThrow); } // will work the same way as with the regular synchronous callback const promise3 = Promise.try(asyncCallback, 'some result', false); const promise4 = Promise.try(asyncCallback, 'some error', true); promise3.then((value) => console.log(value)); // some result promise4.catch((value) => console.log(value)); // some error

This feature is relatively new, so use it with caution.

Object.groupBy() / Map.groupBy()

Both Object.groupBy() and Map.groupBy() group the elements of the iterable by the return value of the provided callback that is called for each element.

const people = [ { name: "John", age: 20, }, { name: "Tom", age: 25, }, { name: "Bob", age: 20, }, { name: "Jack", age: 25, }, { name: "Mike", age: 26, }, ]; console.log(Object.groupBy(people, person => person.age)); /* { "20": [{ name: "John", age: 20, },{ name: "Bob", age: 20, }], "25": [{ name: "Tom", age: 25, },{ name: "Jack", age: 25, }], "26": [{ name: "Mike", age: 26, }], } */ console.log(Map.groupBy(people, person => person.age)); /* Map(3) { 20 => [{ name: "John", age: 20, },{ name: "Bob", age: 20, }], 25 => [{ name: "Tom", age: 25, },{ name: "Jack", age: 25, }], 26 => [{ name: "Mike", age: 26, }], } */

Custom iterator

It's not so new feature but using a combo of generator functions and Symbol.iterator you can easily create an object that can be used with spread operator (...) or iterated with for of loop.

class IterableObject { prop1; prop2; prop3; constructor(prop1, prop2, prop3) { this.prop1 = prop1; this.prop2 = prop2; this.prop3 = prop3; } * [Symbol.iterator]() { yield this.prop1; yield this.prop2; yield this.prop3; } } const obj = new IterableObject('one', 'two', 'three'); for (const x of obj) { console.log(x); } /* one two three */ console.log(...obj); // one two three console.log([...obj]); // ['one', 'two', 'three']

Symbol.hasInstance

This is also not new, but it's an amazing feature that nobody is talking about. With symbol Symbol.hasInstance you can override the behavior of operator instanceof. Normally instanceof checks if the variable is an instance or derivative of some class. However with this symbol you can define a custom logic for instanceof operator and do whatever you want. For example, with this we can check if some object has a valid structure: class User { name = ""; age = 0; static [Symbol.hasInstance](instance) { return typeof instance?.name === "string" && typeof instance?.age === "number"; } } const incompleteUser = { name: "Bob", }; const completeUser = { name: "Bob", age: 20, }; console.log(null instanceof User); // false, we also handle null with ?. operator console.log(incompleteUser instanceof User); // false, because age is undefined console.log(completeUser instanceof User); // true, despite that it's not a real instance of User

WeakRef

Some people know about WeakMap or WeakSet, but even fewer people know about WeakRef. WeakRef, as its name suggests, keeps a weak reference on an object. The object can be retrieved via deref() method of the weak reference as long as the object is still in the memory and isn't garbage collected yet. If the object is garbage collected deref() will return undefined. Here is an example on how it works:

let obj = {}; const weakRef = new WeakRef(obj); const interval = setInterval(() => { // notice that we stringify the object, otherwise it will remain accessible from the dev console console.log(`obj: ${weakRef.deref()}`) if (!weakRef.deref()) { console.log("obj is garbage collected"); clearInterval(interval); } }, 200); // after 1s we make obj unaccessible, so the garbage collector will eventually collect it setTimeout(() => obj = null, 1000);

However, this feature is not recommended to use unless it's extremely necessary. Garbage collection is complicated and different JavaScript engines can use different techniques. This can cause some behaviors that are not specified in the documentation. So, please avoid writing any logic that relies on this as much as possible. Just know that such thing exists.

FinalizationRegistry

FinalizationRegistry is a somewhat similar story to the WeakRef, but this is about firing an event on garbage collection instead of accessing it before the object is garbage collected.

let obj1 = {}; let obj2 = {}; let obj3 = {}; const registry = new FinalizationRegistry((associatedValue) => { console.log(`${associatedValue} is garbage collected`); }); // The second parameters are the associated values which will be passed to the callback, // so we can know which object is deleted. Obviously, this cannot be the object itself, // because otherwise it will not be garbage collected as the registry holds a reference on it. // The associated values can be any primitive or non primitive values that have no reference on the // target objects. registry.register(obj1, "obj1"); registry.register(obj2, "obj2"); // The third optional parameter is unregistration token, we need to pass it to unregister method if we decide // to remove the listener later. It must be an object (can also be the target object) // or a non-registered symbol. If not provided, the listener cannot be unregistered. registry.register(obj3, "obj3", obj3); // after 1s we make obj1, obj2 and obj3 unaccessible, so the garbage collector will eventually collect them setTimeout(() => obj1 = obj2 = obj3 = null, 1000); // after 0.5s we remove the listener for obj3, so the callback will not be called for obj3 setTimeout(() => registry.unregister(obj3), 500);

As with WeakRef, this feature is not recommended to use unless it's extremely necessary. Again, avoid writing any logic that relies on this as much as possible. Just know that such thing exists.

Loop labels

Have you ever been in a situation where you have written nested loops and tried to do break or continue for the outer loop while being in the inner loop? You would probably write something like this:

let breakTheOuterLoop = false; for (let i = 0; i < 10; ++i) { for (let j = 0; j < 10; ++j) { if (i * j > 2) { breakTheOuterLoop = true; break; } console.log(i, j); } if (breakTheOuterLoop) { break; } } /* 0 0 0 1 0 2 0 3 0 4 0 5 0 6 0 7 0 8 0 9 1 0 1 1 1 2 */

JavaScript allows to label the loops, so that you can write break someLabelName or continue someLabelName inside a deeply nested loop which will break or continue that labeled outer loop. The code above can be rewritten like this:

outerLoop: for (let i = 0; i < 10; ++i) { for (let j = 0; j < 10; ++j) { if (i * j > 2) { break outerLoop; } console.log(i, j); } } /* 0 0 0 1 0 2 0 3 0 4 0 5 0 6 0 7 0 8 0 9 1 0 1 1 1 2 */

Constructor return value

In JavaScript constructors allow to return a custom object instead of returning the default object. This might be useful, for example, when you want to return an already existing object because you want to work with the same resource. This is how it works:

class DomElementWrapper { static #existingWrappers = new WeakMap(); #wrappedElement; constructor(element) { if (DomElementWrapper.#existingWrappers.has(element)) { return DomElementWrapper.#existingWrappers.get(element); } this.#wrappedElement = element; DomElementWrapper.#existingWrappers.set(element, this); } getWrappedElement() { return this.#wrappedElement; } } console.log(new DomElementWrapper(document.body) === new DomElementWrapper(document.body)); // true, because they wrap the same object console.log(new DomElementWrapper(document.documentElement) === new DomElementWrapper(document.body)); // false, because they wrap different objects

The returned value must be either object / function or undefined (this one will cause the default behavior), otherwise it will return an object if it's not a derived class or will throw TypeError otherwise.





Read previous