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

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 symbolSymbol.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.