Back

JavaScript Patterns That Age Well

Note: This is a test, not an actual blog

Patterns age differently. Some feel clever and become burdens. Others are boring by design and stay readable for years. Here are the ones that compound positively.

Option Objects for Function Arguments

Functions with multiple arguments are fragile. Callers must remember the order. Adding a new argument is a breaking change.

// Fragile — order matters, booleans are opaque
function createServer(host, port, keepAlive, timeout) {}
createServer('localhost', 3000, true, 5000);

// Better — self-documenting, order-independent, easily extensible
function createServer({ host = 'localhost', port = 3000, keepAlive = true, timeout = 5000 } = {}) {}
createServer({ port = 8080, timeout = 10000 });

Destructuring with defaults makes the signature both self-documenting and forward-compatible.

Result Types over Exceptions

Exceptions are control flow that doesn’t appear in a function’s signature. A function that throws is a surprise — the caller must read the source or documentation to know it can fail.

// Returns a Result — success or failure is always explicit
function parseConfig(raw) {
  try {
    return { ok: true, value: JSON.parse(raw) };
  } catch (err) {
    return { ok: false, error: err.message };
  }
}

const result = parseConfig(input);
if (!result.ok) {
  console.error('Config invalid:', result.error);
  return;
}

// result.value is safely available here
doSomething(result.value);

The caller cannot accidentally ignore the error case. The types tell the story. This is the pattern fetch should have used.

Immutable Updates

Mutating objects in-place makes state hard to track and debug. When you see a bug, you have to reconstruct the sequence of mutations. Immutable updates create a clear trail.

// Mutation — what does 'user' look like now? Depends on history.
user.settings.notifications = true;

// Immutable — the old value is unchanged, the new one is explicit
const updatedUser = {
  ...user,
  settings: {
    ...user.settings,
    notifications: true,
  },
};

For deeply nested structures, this gets verbose. That’s feedback from the design — the shape is too deep. Flatter state structures are easier to update immutably.

The Module Pattern Without Classes

Classes in JavaScript carry implicit this binding, subclassing complexity, and the ceremony of instantiation. For most utility modules, a plain function that returns an object is simpler.

function createQueue(maxSize = Infinity) {
  const items = [];

  return {
    enqueue(item) {
      if (items.length >= maxSize) throw new Error('Queue full');
      items.push(item);
    },
    dequeue() {
      if (items.length === 0) throw new Error('Queue empty');
      return items.shift();
    },
    get size() { return items.length; },
    get isEmpty() { return items.length === 0; },
  };
}

const q = createQueue(10);
q.enqueue('task-1');
console.log(q.size); // 1

No new, no this, no prototype chain to reason about. The items array is private by closure — it cannot be accessed from outside.

Async with Promise.allSettled

Promise.all fails fast — if any promise rejects, the whole thing rejects. That’s rarely what you want when processing a batch of independent operations.

const urls = ['/api/users', '/api/posts', '/api/comments'];

const results = await Promise.allSettled(urls.map(url => fetch(url).then(r => r.json())));

for (const result of results) {
  if (result.status === 'fulfilled') {
    process(result.value);
  } else {
    console.warn('Failed:', result.reason);
  }
}

Each result is independently either { status: 'fulfilled', value } or { status: 'rejected', reason }. You process what succeeded and handle what failed — without one failure poisoning the whole batch.


The common thread: these patterns make the code’s behavior visible at the call site. They trade cleverness for clarity, and clarity compounds over time.