Home / Articles

Auditing utils.js: Six Built-ins That Replace Hand-Rolled Helpers

See which native JavaScript APIs can replace deep-clone hacks, defensive copies, manual timeouts, deferred promises and lodash groupBy, and what caveats apply.

Publisher: REACTAPP.TOP5 min read1052 words

This article is published in English.

Sources: 6 Modern JavaScript Features You’re Probably Still Working Around

Many codebases still ship helper functions and library imports for problems the platform solved years ago. These additions landed quietly, so habits formed around the old workarounds never got revisited. Below are six built-ins worth checking your utils.js against, with the pitfall each one removes and the limits you should know before you delete anything.

structuredClone() instead of the JSON round-trip

The most common homemade deep copy serializes an object and parses it back:

const copy = JSON.parse(JSON.stringify(original));

It breaks on anything JSON cannot represent: Date instances come back as strings, properties set to undefined are dropped, Map and Set collapse into empty objects without any warning, and a circular reference makes JSON.stringify throw.

structuredClone() is a global function available in modern browsers and Node.js, with nothing to import. Take an object holding a date, a set and a map:

const original = {
  date: new Date(),
  items: new Set([1, 2, 3]),
  meta: new Map([['key', 'value']]),
};

Cloning it keeps every one of those types intact, and cyclic structures are copied correctly too:

const copy = structuredClone(original);
// Dates, Sets, Maps, and circular refs all survive intact

It uses the structured clone algorithm behind postMessage and IndexedDB, so it covers edge cases hand-written clones miss. It does have boundaries: functions and DOM nodes cannot be cloned and raise an error, and instances of your own classes come back as plain objects without their prototype.

toSorted() and toReversed() instead of defensive copies

.sort() and .reverse() mutate in place, so sorting an array shared by reference changes it everywhere. Starting from this array:

const scores = [30, 10, 20];

toSorted() returns a new, sorted array and leaves the source alone:

const sorted = scores.toSorted((a, b) => a - b);console.log(scores); // [30, 10, 20] — untouched
console.log(sorted);  // [10, 20, 30]

toReversed() and toSpliced() follow the same copy-returning pattern. If your code is full of [...arr].sort() to keep state immutable, as React reducers often are, the spread is no longer necessary. A broader tour of these copy-returning methods is in modern JavaScript array methods that replace boilerplate.

Object.hasOwn() instead of hasOwnProperty

Calling the method through the object hides a trap:

obj.hasOwnProperty('key'); // breaks if obj = Object.create(null)

An object made with Object.create(null) has no prototype, so it never inherits hasOwnProperty and the call throws a TypeError. The same happens if an object defines its own property with that name. Object.hasOwn() is a static function, so it does not depend on what the object inherits:

Object.hasOwn(obj, 'key'); // works regardless of the object's prototype chain

The swap is tiny, but it prevents failures that surface only in production, when a caller passes an object shape nobody planned for.

AbortSignal.timeout() instead of a manual timer

Giving fetch a deadline used to require an AbortController plus a setTimeout that triggers it:

const controller = new AbortController();
const id = setTimeout(() => controller.abort(), 5000);

You then had to remember to clear the timer once the request settled:

fetch(url, { signal: controller.signal }).finally(() => clearTimeout(id));

The signal can now carry its own deadline:

fetch(url, { signal: AbortSignal.timeout(5000) });

One line, no cleanup, no leftover timer. When it fires, the request rejects with a TimeoutError DOMException, which lets you tell a timeout apart from a user-triggered abort. To combine a timeout with another cancellation source, look at AbortSignal.any() in current documentation.

Promise.withResolvers() instead of leaking resolve and reject

Sometimes a promise has to be settled from outside its executor. The classic workaround declares variables and assigns them inside the constructor callback:

let resolve, reject;
const promise = new Promise((res, rej) => {
  resolve = res;
  reject = rej;
});

It works, but smuggling functions out of a closure is easy to misread. Promise.withResolvers() returns the promise and both settle functions together:

const { promise, resolve, reject } = Promise.withResolvers();

This fits naturally when you queue asynchronous work or wrap an event-driven API in promises. For a larger worked example, see coordinating LLM tool calls with Promise.withResolvers.

Object.groupBy() instead of lodash groupBy

Grouping records by a key is one of the most frequent reasons to import a utility library. Given a list of people:

const people = [
  { name: 'Sam', age: 21 },
  { name: 'Lee', age: 34 },
  { name: 'Kai', age: 21 },
];

Object.groupBy() takes an iterable and a callback, and returns an object with one array per key:

const byAge = Object.groupBy(people, (p) => p.age);
// { 21: [Sam, Kai], 34: [Lee] }

Note the name. An earlier proposal called this Array.prototype.group(), but it was renamed and moved to the static Object.groupBy() (plus Map.groupBy() when keys are not strings) before standardization, and only the static forms ship. The result has a null prototype, and it is the newest API in this list, so check your minimum supported browser and Node.js versions before you remove the dependency.

Before you delete the helpers

None of these changes your architecture, but each removes a bug-prone pattern or an unneeded dependency. A sensible order:

  • Check support on caniuse.com or MDN against the environments you actually target, since some of these are newer than others and older browsers may lack them.
  • Keep polyfills until your support matrix really allows dropping them.
  • Replace call sites gradually, with tests around cloning and grouping where output shape matters.
  • Remove the old helper or library import only once nothing references it.