Pure Functions
A pure function has two properties: given the same inputs it always returns the same output, and it produces no side effects. That is a small definition with big consequences — pure functions are easier to test, easier to reason about, safe to cache, and safe to call from many places at once. Most "clean code" advice in modern JavaScript boils down to "push side effects to the edges and keep the middle pure".
The two rules
Deterministic — same input → same output, every time. The function depends only on its arguments, not on any external state.
No side effects — does not modify variables outside its scope, does not mutate its arguments, does not write to the screen/disk/network, does not log, does not throw based on state outside its control.
A pure function
function add(a, b) {
return a + b;
}
console.log(add(2, 3)); // 5 — always 5, no matter how many times you calladd looks at only a and b, returns a value, and changes nothing else in the world. It is the simplest possible pure function.
An impure function — non-deterministic
function withTimestamp(message) {
return Date.now() + ": " + message;
}
console.log(withTimestamp("ok"));
console.log(withTimestamp("ok")); // different result each callThe same input produces different outputs because Date.now() is a moving target. Useful in real code, but impossible to test by simple equality.
An impure function — side effect
let total = 0;
function addToTotal(n) {
total += n; // mutates outside state
return total;
}
addToTotal(5);
addToTotal(5);
console.log(total); // 10 — depends on call historyReturning the right value is only half the job. Modifying total makes the function's behaviour depend on how many times it has been called before, and silently affects code elsewhere.
Mutating an argument is also impure
// Impure — mutates the input array
function addItemBad(list, item) {
list.push(item);
return list;
}
// Pure — returns a new array
function addItemGood(list, item) {
return [...list, item];
}
const a = [1, 2, 3];
addItemBad(a, 4);
console.log(a); // [1, 2, 3, 4] — surprise! the caller's array changedIn modern code (especially React, Redux, and any functional style) the pure version is almost always preferred. The caller is in control of when state changes; the function just computes a value.
Why purity matters
Testability — pure functions only need inputs and an expected output. No mocks, no setup, no teardown.
Reasoning — you can read a pure function in isolation. There is no "what state must be true elsewhere for this to work?".
Memoisation — same inputs → same outputs, so the result can be safely cached.
Parallelism — pure functions are safe to run concurrently because they share no state.
Refactoring — moving, inlining, or extracting a pure function never changes program behaviour.
Side effects are not the enemy — they just need a home
Real programs must do impure things: log messages, fetch data, write files, render to the DOM. The goal is not to eliminate side effects but to isolate them. Keep the core of your logic pure, and let a thin outer layer handle the messy I/O.
// Pure — easy to test
function buildGreeting(user) {
return "Hello, " + (user.name ?? "friend");
}
// Impure — does I/O
function showGreeting(user) {
console.log(buildGreeting(user)); // side effect lives here
}
showGreeting({ name: "Ada" });Tests can call buildGreeting directly without needing to capture console output, replace the DOM, or stub a network call.
A handy mental check
For any function, ask yourself two quick questions:
If I delete every other line of code in the project, can this function still produce the same output for the same input?
After running this function, is anything different in the world — variables, DOM, disk, screen, network?
A "yes" to the first and "no" to the second means the function is pure.