hey jonny! đź‘‹

Modern JavaScript Features That Will Change How You Code

JavaScript has evolved dramatically over the past few years. If you’ve been stuck in ES5 land or even early ES6, you’re missing out on some incredible features that can transform how you write code. Let’s explore the modern JavaScript features that have become essential tools in my daily development workflow.

Optional Chaining (?.) - Say Goodbye to Nested Checks

Before optional chaining, accessing nested object properties was a minefield:

// The old, verbose way
if (user && user.profile && user.profile.address && user.profile.address.street) {
  console.log(user.profile.address.street);
}

// Or using try/catch (yikes!)
try {
  console.log(user.profile.address.street);
} catch (e) {
  console.log('Address not available');
}

Now we can do this elegantly:

// Clean and safe
console.log(user?.profile?.address?.street);

// Works with arrays too
console.log(users?.[0]?.name);

// Even with method calls
api?.getData?.();

This single feature has eliminated hundreds of lines of defensive code from my projects.

Nullish Coalescing (??) - The Perfect Default Operator

The logical OR operator (||) has a problem—it treats falsy values like 0, false, and empty strings as “missing” values:

// Problematic with ||
const userAge = user.age || 18; // What if age is 0?
const isVisible = user.visible || true; // What if visible is false?

// Nullish coalescing only checks for null/undefined
const userAge = user.age ?? 18; // Only defaults if age is null/undefined
const isVisible = user.visible ?? true; // Preserves false values

This is especially useful for API responses where 0 or false are valid values.

Template Literals - Beyond Simple String Interpolation

Template literals aren’t just for variable substitution. They enable powerful patterns:

Multi-line Strings

const htmlTemplate = `
  <div class="user-card">
    <h2>${user.name}</h2>
    <p>${user.bio}</p>
  </div>
`;

Tagged Template Literals

function highlight(strings, ...values) {
  return strings.reduce((result, string, i) => {
    const value = values[i] ? `<mark>${values[i]}</mark>` : '';
    return result + string + value;
  }, '');
}

const searchTerm = 'JavaScript';
const text = highlight`Learn ${searchTerm} programming with ease!`;
// Result: "Learn <mark>JavaScript</mark> programming with ease!"

Destructuring - Extract What You Need

Destructuring assignment makes working with objects and arrays incredibly clean:

Object Destructuring with Defaults

const { name, age = 25, email: userEmail } = user;

// Nested destructuring
const { address: { city, country = 'US' } = {} } = user;

// Rest properties
const { password, ...publicUser } = user;

Array Destructuring Tricks

// Skip elements you don't need
const [first, , third] = array;

// Swap variables
[a, b] = [b, a];

// Rest elements
const [head, ...tail] = array;

Function Parameter Destructuring

function createUser({ name, email, role = 'user' }) {
  return { id: Date.now(), name, email, role };
}

createUser({ name: 'John', email: 'john@example.com' });

Spread Operator (…) - The Swiss Army Knife

The spread operator is incredibly versatile:

Array Operations

// Merge arrays
const combined = [...array1, ...array2];

// Clone arrays
const cloned = [...original];

// Convert NodeList to Array
const elements = [...document.querySelectorAll('.item')];

// Find max value
const max = Math.max(...numbers);

Object Operations

// Merge objects (right-most wins)
const merged = { ...defaultConfig, ...userConfig };

// Clone objects (shallow)
const cloned = { ...original };

// Add properties
const enhanced = { ...user, lastLogin: new Date() };

Async/Await - Promises Made Simple

Async/await transformed asynchronous JavaScript from callback hell to readable code:

// Promise chains (still valid, but verbose)
fetch('/api/user')
  .then(response => response.json())
  .then(user => fetch(`/api/posts/${user.id}`))
  .then(response => response.json())
  .then(posts => console.log(posts));

// Async/await (much cleaner)
async function getUserPosts() {
  try {
    const userResponse = await fetch('/api/user');
    const user = await userResponse.json();
    
    const postsResponse = await fetch(`/api/posts/${user.id}`);
    const posts = await postsResponse.json();
    
    return posts;
  } catch (error) {
    console.error('Failed to fetch user posts:', error);
  }
}

Top-Level Await

In modern environments, you can use await at the top level:

// No need to wrap in an async function
const config = await fetch('/api/config').then(r => r.json());
const module = await import('./dynamic-module.js');

console.log('App initialized with config:', config);

Array Methods That Changed Everything

Array.from() - Create Arrays from Anything

// Create arrays from array-like objects
const nodeArray = Array.from(document.querySelectorAll('.item'));

// Generate sequences
const numbers = Array.from({ length: 5 }, (_, i) => i + 1); // [1, 2, 3, 4, 5]

// Transform during creation
const doubled = Array.from([1, 2, 3], x => x * 2); // [2, 4, 6]

Powerful Array Methods

// includes() - better than indexOf
const hasAdmin = users.some(user => user.role === 'admin');

// find() and findIndex()
const admin = users.find(user => user.role === 'admin');
const adminIndex = users.findIndex(user => user.role === 'admin');

// flatMap() - map and flatten in one step
const allTags = posts.flatMap(post => post.tags);

Map and Set - Better Data Structures

Map - Objects with Superpowers

const userCache = new Map();

// Any type as key
userCache.set(userObject, userData);
userCache.set('string-key', value);
userCache.set(42, numericData);

// Iteration maintains insertion order
for (const [key, value] of userCache) {
  console.log(key, value);
}

// Size property
console.log(userCache.size);

Set - Unique Values Collection

const uniqueIds = new Set();

uniqueIds.add(1);
uniqueIds.add(1); // No duplicates
uniqueIds.add(2);

// Remove duplicates from array
const unique = [...new Set(arrayWithDuplicates)];

// Set operations
const setA = new Set([1, 2, 3]);
const setB = new Set([3, 4, 5]);

// Intersection
const intersection = new Set([...setA].filter(x => setB.has(x)));

// Union
const union = new Set([...setA, ...setB]);

Private Class Fields - True Encapsulation

class BankAccount {
  #balance = 0; // Private field
  #accountNumber; // Private field
  
  constructor(initialBalance, accountNumber) {
    this.#balance = initialBalance;
    this.#accountNumber = accountNumber;
  }
  
  #validateTransaction(amount) { // Private method
    return amount > 0 && amount <= this.#balance;
  }
  
  withdraw(amount) {
    if (this.#validateTransaction(amount)) {
      this.#balance -= amount;
      return true;
    }
    return false;
  }
  
  get balance() {
    return this.#balance;
  }
}

const account = new BankAccount(1000, '123456');
// account.#balance; // SyntaxError - private field

Dynamic Imports - Load Code on Demand

// Load modules conditionally
if (user.preferences.theme === 'dark') {
  const darkTheme = await import('./themes/dark.js');
  darkTheme.apply();
}

// Load heavy libraries only when needed
async function handleChartData(data) {
  const { Chart } = await import('chart.js');
  return new Chart(canvas, { data });
}

// Dynamic imports return promises
import('./module.js')
  .then(module => module.doSomething())
  .catch(err => console.error('Failed to load module'));

Practical Examples: Putting It All Together

API Client with Modern JavaScript

class ApiClient {
  #baseUrl;
  #defaultHeaders;
  
  constructor(baseUrl, options = {}) {
    this.#baseUrl = baseUrl;
    this.#defaultHeaders = {
      'Content-Type': 'application/json',
      ...options.headers
    };
  }
  
  async #request(endpoint, options = {}) {
    const url = `${this.#baseUrl}${endpoint}`;
    const config = {
      headers: { ...this.#defaultHeaders, ...options.headers },
      ...options
    };
    
    try {
      const response = await fetch(url, config);
      
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }
      
      return await response.json();
    } catch (error) {
      console.error('API request failed:', error);
      throw error;
    }
  }
  
  async get(endpoint, params = {}) {
    const searchParams = new URLSearchParams(params);
    const url = searchParams.toString() ? `${endpoint}?${searchParams}` : endpoint;
    return this.#request(url);
  }
  
  async post(endpoint, data) {
    return this.#request(endpoint, {
      method: 'POST',
      body: JSON.stringify(data)
    });
  }
}

Smart Form Validation

class FormValidator {
  static #validationRules = new Map([
    ['email', /^[^\s@]+@[^\s@]+\.[^\s@]+$/],
    ['phone', /^\+?[\d\s-()]+$/],
    ['url', /^https?:\/\/.+/]
  ]);
  
  static validate(formData) {
    const errors = [];
    
    for (const [field, value] of Object.entries(formData)) {
      const rule = this.#validationRules.get(field);
      
      if (rule && !rule.test(value ?? '')) {
        errors.push(`Invalid ${field} format`);
      }
    }
    
    return {
      isValid: errors.length === 0,
      errors
    };
  }
}

// Usage
const result = FormValidator.validate({
  email: user?.contact?.email,
  phone: user?.contact?.phone ?? ''
});

if (!result.isValid) {
  console.log('Validation errors:', result.errors);
}

Browser Support and Migration Strategy

Most modern JavaScript features have excellent browser support:

Migration Tips

  1. Start with TypeScript: Get modern JavaScript features with compile-time safety
  2. Use Babel: Transpile modern features for older browsers
  3. Progressive enhancement: Use feature detection for advanced APIs
  4. ESLint rules: Enforce modern patterns and catch outdated code

The Impact on Code Quality

These features aren’t just syntactic sugar—they fundamentally improve code quality:

Looking Forward

JavaScript continues to evolve rapidly. Features like:

Are on the horizon and will further improve the developer experience.

Conclusion

Modern JavaScript features aren’t just nice-to-haves—they’re essential tools for writing maintainable, readable, and robust code. If you haven’t adopted these patterns yet, start today. Your future self (and your teammates) will thank you.

The JavaScript ecosystem moves fast, but these features have proven their staying power. They’re not experimental—they’re the new foundation of professional JavaScript development.


Ready to modernize your JavaScript? Start by refactoring one component using optional chaining and destructuring. You’ll be amazed at how much cleaner your code becomes.