DEV Community

Software Solutions
Software Solutions

Posted on

Modern JavaScript Features Every Developer Should Know

1. Deep Cloning Objects with structuredClone()

Historically, deep-cloning an object meant either reaching for JSON.parse(JSON.stringify(obj))—which fails on Date objects, Set, Map, undefined, or circular references—or importing an external library like Lodash (_.cloneDeep).

Native JavaScript now includes structuredClone(), which natively handles deep copies, circular references, and built-in data structures seamlessly.

const userProfile = {
  name: "Alex",
  joined: new Date(),
  preferences: {
    theme: "dark",
    notifications: true,
  },
  roles: new Set(["admin", "editor"]),
};

//  Native deep copy!
const clonedProfile = structuredClone(userProfile);

clonedProfile.preferences.theme = "light";
clonedProfile.roles.add("viewer");

console.log(userProfile.preferences.theme); // "dark" (unmodified)
console.log(userProfile.roles.has("viewer")); // false (unmodified)

Enter fullscreen mode Exit fullscreen mode

2. Dynamic Array Indexing with .at()

Accessing elements near the end of an array in classic JavaScript required awkward indexing like array[array.length - 1].

The .at() method works on Arrays and Strings, accepting negative integers to count backward from the end.

Javascript

const frameworkStack = ["React", "Vue", "Angular", "Svelte", "Next.js"];

// ❌ Old Way
const lastItem = frameworkStack[frameworkStack.length - 1]; // "Next.js"

// ✅ Modern Way
const lastItemModern = frameworkStack.at(-1); // "Next.js"
const secondToLast = frameworkStack.at(-2);  // "Svelte"
Enter fullscreen mode Exit fullscreen mode

3. Asynchronous Execution with Top-Level await

Previously, using await outside an async function would trigger a syntax error. Modern JavaScript allows Top-Level Await inside ES modules (type="module"), making resource initialization and dynamic imports significantly cleaner.

Javascript

// dbConnection.js (ES Module)
import { initializeDatabase } from "./config/database.js";

// ✅ No need to wrap in an async IIFE anymore!
export const db = await initializeDatabase();
Enter fullscreen mode Exit fullscreen mode

You can also use top-level await to gracefully handle dynamic module loading:

Javascript

// Dynamically load polyfills or heavy modules on demand
const { chartEngine } = await import("./analytics/chartEngine.js");
Enter fullscreen mode Exit fullscreen mode

4. Safer Object Property Inspections with Object.hasOwn()

Using Object.prototype.hasOwnProperty.call(obj, prop) was the standard safe way to check if an object possessed a direct property rather than inheriting it through the prototype chain.

Modern JS simplifies this with Object.hasOwn(), offering a cleaner and more robust replacement.

Javascript

const userPermissions = Object.create(null); // Object without prototype
userPermissions.canEdit = true;

// ❌ Fails with TypeError: userPermissions.hasOwnProperty is not a function
// userPermissions.hasOwnProperty("canEdit"); 

// ✅ Works safely every time
if (Object.hasOwn(userPermissions, "canEdit")) {
  console.log("User has editing rights.");
}
Enter fullscreen mode Exit fullscreen mode

5. Clean Error Handling with Error Cause (new Error(msg, { cause }))

When catching and re-throwing errors in complex workflows (such as API interactions or database operations), preserving the original underlying issue used to require custom Error classes.

Modern JavaScript supports an optional cause property when instantiating standard Error objects:

Javascript

async function fetchUserData(userId) {
  try {
    return await api.get(`/users/${userId}`);
  } catch (networkError) {
    // Wrap the error with context while preserving the original cause stack
    throw new Error(`Failed to load profile for User ${userId}`, { cause: networkError });
  }
}

// Handling the error downstream:
try {
  await fetchUserData(404);
} catch (error) {
  console.error(error.message); // "Failed to load profile for User 404"
  console.error(error.cause);   // Original network error details
}
Enter fullscreen mode Exit fullscreen mode

6. Non-Mutating Array Operations (.toSorted, .toSpliced, .toReversed)

Traditionally, methods like .sort(), .splice(), and .reverse() mutated the array in place, requiring developers to spread/shallow-copy the array first to maintain immutability.

Modern ECMAScript introduces non-mutating alternatives that return a fresh copy:

Javascript

const scores = [40, 10, 100, 25];

// ❌ Mutates original array:
// scores.sort((a, b) => a - b);

// ✅ Returns a new sorted array without modifying 'scores'
const sortedScores = scores.toSorted((a, b) => a - b);

console.log(scores);       // [40, 10, 100, 25] (unmodified)
console.log(sortedScores); // [10, 25, 40, 100]
Enter fullscreen mode Exit fullscreen mode

Developer Takeaways

  1. Adopt Built-In Methods: Replace manual utility functions and heavy NPM utilities with native solutions like structuredClone(), Object.hasOwn(), and non-mutating array methods.

  2. Clean Up Error Traces: Use { cause } when throwing errors across application layers to streamline debugging in production logs.

  3. Write Safer Code: Use .at(-1) for array indexing and non-mutating array routines to avoid subtle state bugs.

Need Custom Web Applications or Enterprise Software Solutions?

Building modern, scalable web applications requires clean frontend logic combined with high-performance backend architecture.

👉 Partner with Software Solutions for end-to-end web application development, full-stack software architecture, API integrations, and cloud infrastructure tailored to scale your software products seamlessly.

Top comments (0)