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.
