mdashikjs/blog
All posts
Pure Functions Changed How I Write Production JavaScript
Frontend

Pure Functions Changed How I Write Production JavaScript

Frontend4 min

Pure Functions Changed How I Write Production JavaScript

A pure function does not reach outside itself, does not remember, does not surprise. It sounds austere until you have to refactor 2000 lines of side-effecty code at 2am.

JavaScriptFunctional ProgrammingTesting
Share:

The definition, minus the dogma

Pure means: same input, same output, and no observable effect on anything outside the function. Reading a global, calling Date.now(), mutating an argument, logging — all break purity.

// impure — reads the clock
function isExpired(token) {
  return token.expiresAt < Date.now();
}

// pure — time is an argument
function isExpired(token, now) {
  return token.expiresAt < now;
}

The pure version tests without mocking the clock. That is the payoff, in one line.

The boring truth about pure code

Most of a web app cannot be pure. You read a database, you send emails, you write to disk. What can be pure is the logic that decides what to do — the transformations, the validations, the branching. Push effects to the edges; keep decisions in the middle.

A real refactor

Before:

async function chargeOrder(orderId) {
  const order = await db.orders.get(orderId);
  const discount = order.couponCode === 'SUMMER' ? 0.1 : 0;
  const total = order.subtotal * (1 - discount) + order.tax;
  if (total > 1000) logger.warn('large charge');
  await stripe.charge(total);
  await db.orders.update(orderId, { chargedAt: new Date() });
}

Everything is tangled. To test the discount logic I would need a fake DB, a fake Stripe, a fake logger.

After:

function computeChargeTotal(order) {
  const discount = order.couponCode === 'SUMMER' ? 0.1 : 0;
  return order.subtotal * (1 - discount) + order.tax;
}

async function chargeOrder(orderId) {
  const order = await db.orders.get(orderId);
  const total = computeChargeTotal(order);
  if (total > 1000) logger.warn('large charge');
  await stripe.charge(total);
  await db.orders.update(orderId, { chargedAt: new Date() });
}

computeChargeTotal is pure. A table-driven test covers every coupon and tax combination in seconds. The effectful wrapper stays thin.

The tell for code that wants to be pure

If you find yourself writing a test that mocks a clock, random number generator, UUID, or environment variable — that is a function wanting to be pure. Lift the value to an argument. Let the caller decide.

// before
function createSession(userId) {
  return { id: crypto.randomUUID(), userId, createdAt: Date.now() };
}

// after
function createSession(userId, { id, now }) {
  return { id, userId, createdAt: now };
}

In production the caller passes crypto.randomUUID() and Date.now(). In tests the caller passes fixed values. The logic never needs to know.

Where I draw the line

I do not chase purity for its own sake. Logging is a side effect, and I still log from business logic. A short-lived cache counter is mutable and that is fine. Purity is a tool for isolating decisions from the world, not a religion. The payoff is in the places where logic is hard and change is frequent — those are the places to make pure. Everywhere else, pragmatism wins.

MA

Written by Md Ashik

Senior Software Engineer building reliable backends. I write about the practical tradeoffs behind shipping software that holds up in production.