Modern JavaScript: Beyond the Basics

Modern JavaScript features that go beyond tutorials, the patterns that separate intermediate from advanced developers.

• 10 min read
JavaScriptWeb DevelopmentAdvancedES2024

Modern JavaScript: Beyond the Basics

JavaScript has changed a lot in the last few years. Here are the patterns I reach for most, past the basics everyone already knows.

Advanced async patterns

Promise Combinators

Beyond Promise.all, modern JavaScript has several other combinators:

// Promise.allSettled - Wait for all, regardless of success/failure
const results = await Promise.allSettled([
  fetch('/api/user'),
  fetch('/api/posts'),
  fetch('/api/comments'),
]);

// Promise.race - First to finish wins
const fastest = await Promise.race([
  fetch('/api/server1'),
  fetch('/api/server2'),
]);

// Promise.any - First success wins (ignores failures)
const firstSuccess = await Promise.any([
  fetchFromCDN1(),
  fetchFromCDN2(),
  fetchFromCDN3(),
]);

Async iteration

async function* generateData() {
  for (let i = 0; i < 5; i++) {
    await delay(1000);
    yield i;
  }
}

// Consume with for-await-of
for await (const value of generateData()) {
  console.log(value); // 0, 1, 2, 3, 4 (with 1s delays)
}

Advanced object patterns

Proxy for validation

const createValidatedObject = (schema) => {
  return new Proxy(
    {},
    {
      set(target, property, value) {
        const validator = schema[property];
        if (!validator || validator(value)) {
          target[property] = value;
          return true;
        }
        throw new Error(`Invalid value for ${property}`);
      },
    }
  );
};

const user = createValidatedObject({
  age: (val) => typeof val === 'number' && val > 0,
  email: (val) => /\S+@\S+\.\S+/.test(val),
});

user.age = 25; // OK
user.email = 'test@example.com'; // OK
user.age = -5; // Throws error

Private fields

class BankAccount {
  #balance = 0; // Private field

  deposit(amount) {
    if (amount > 0) {
      this.#balance += amount;
    }
  }

  getBalance() {
    return this.#balance;
  }
}

const account = new BankAccount();
account.deposit(100);
console.log(account.getBalance()); // 100
console.log(account.#balance); // SyntaxError

Functional patterns

Composition and pipe

const compose =
  (...fns) =>
  (x) =>
    fns.reduceRight((acc, fn) => fn(acc), x);

const pipe =
  (...fns) =>
  (x) =>
    fns.reduce((acc, fn) => fn(acc), x);

// Usage
const addOne = (x) => x + 1;
const double = (x) => x * 2;
const square = (x) => x * x;

const calculate = pipe(
  addOne, // 6
  double, // 12
  square // 144
);

console.log(calculate(5)); // 144

Currying

const curry = (fn) => {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    }
    return (...nextArgs) => curried(...args, ...nextArgs);
  };
};

const add = (a, b, c) => a + b + c;
const curriedAdd = curry(add);

console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
console.log(curriedAdd(1)(2, 3)); // 6

Performance optimization

Memoization

const memoize = (fn) => {
  const cache = new Map();

  return (...args) => {
    const key = JSON.stringify(args);

    if (cache.has(key)) {
      return cache.get(key);
    }

    const result = fn(...args);
    cache.set(key, result);
    return result;
  };
};

const fibonacci = memoize((n) => {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
});

WeakMap for private data

const privateData = new WeakMap();

class User {
  constructor(name, password) {
    this.name = name;
    privateData.set(this, { password });
  }

  verifyPassword(input) {
    return privateData.get(this).password === input;
  }
}

Modern tooling integration

Optional chaining and nullish coalescing

// Optional chaining
const userName = user?.profile?.name ?? 'Guest';

// Nullish coalescing (only null/undefined, not falsy)
const count = response.count ?? 0; // 0 if null/undefined
const isActive = response.isActive ?? true; // true if null/undefined

Top-level await (ES2022)

// In modules, you can use await at the top level
const data = await fetch('/api/config').then((r) => r.json());

export default data;

Conclusion

Most of these patterns solve a specific problem: Proxy catches invalid state before it corrupts your app, WeakMap keeps private data from leaking into a serialized object, memoization skips redundant work. None of them belong in your code by default.

I’ve seen more bugs introduced by someone reaching for currying because it looked clever than by someone writing a boring for loop. Use the advanced tool when the boring one actually fails you, not before.