JAVASCRIPT / FUNCTIONS
Pure functions and predictable outputs
Tell pure functions from impure ones in JavaScript, and rewrite a function so its result depends only on its arguments and its call changes nothing else.
What you will learn
- Check purity two ways: same args, same result, and nothing outside the call changes.
- Return a new array or object instead of mutating a parameter you were handed.
- Spot hidden inputs: outer let bindings, Math.random(), Date.now(), DOM reads.
- Pass time or randomness in as an argument so the logic stays testable.
Understanding Pure functions and predictable outputs
A function is pure when two things hold at once: for a given set of arguments it always returns the same value, and calling it leaves no trace anywhere else in the program. The useful mental model is a lookup table — `pricePure(2, 20, 5)` is just another way of writing `45`, and you could swap one for the other anywhere without changing how the program runs. Anything that breaks that substitution, either a return value that drifts or a change the rest of the code can notice, means the function is impure.
In JavaScript the second condition fails more often than people expect, because an object or array argument hands the function a reference to the caller's data rather than a copy. `o.qty = o.qty + 1` inside `priceImpure` is not local bookkeeping; it edits the very object the caller still holds, which is why calling it twice with the same argument returns 85 the second time instead of 65. The first condition fails whenever a function reads something it was never given: an outer `let`, a module-level object, `Math.random()`, `Date.now()`, `document`, or a response from the network. Those are hidden inputs, and a function with hidden inputs cannot be checked by calling it and comparing the result to an expected value.
Purity is not a rule to apply everywhere, because a program built only from pure functions produces nothing observable — no output, no storage, no pixels. The working pattern is to keep the calculation pure and push the effects into a thin outer layer: read the state, pass it in as arguments, write the returned value back. Mutation inside a function is also fine as long as it only touches data that function itself created, since nothing outside can observe it; `[...list].sort()` mutates an array, and the function around it is still pure.
const order = { item: 'lamp', qty: 2 };
let shippingFlat = 5;
// Impure: edits the caller's object and reads a value it was not given
function priceImpure(o) {
o.qty = o.qty + 1;
return o.qty * 20 + shippingFlat;
}
// Pure: every input arrives as an argument, nothing outside is touched
function pricePure(qty, unit, shipping) {
return qty * unit + shipping;
}
console.log(priceImpure(order));
console.log(priceImpure(order));
console.log(order.qty);
console.log(pricePure(2, 20, 5));
console.log(pricePure(2, 20, 5));A function is pure when its return value is fully determined by its arguments and the call leaves everything else in the program exactly as it was.
Worked examples
sort() ruins a read-only-looking helper
Shows how a function that only reads its parameter can still reorder the caller's array, and how one copy fixes it.
const scores = [5, 1, 4];
function topTwoImpure(list) {
return list.sort((a, b) => b - a).slice(0, 2);
}
function topTwoPure(list) {
return [...list].sort((a, b) => b - a).slice(0, 2);
}
console.log(JSON.stringify(topTwoPure(scores)), JSON.stringify(scores));
console.log(JSON.stringify(topTwoImpure(scores)), JSON.stringify(scores));Example explained
Line 1`list.sort(...)` sorts in place and returns that same array, so `topTwoImpure` rearranges whatever the caller passed in.
Line 2`[...list]` builds a fresh array first, so the sort in `topTwoPure` can only touch the copy.
Line 3After the impure call, `scores` prints as `[5,4,1]`: the original insertion order is gone for good.
Line 4Both functions return the same top two, so the damage stays invisible until some other code depends on the original order.
Move the randomness to the call site
Demonstrates turning a hidden input into a parameter so the interesting boundary cases can be asserted.
// Hidden input: the caller cannot predict or reproduce the result
function rollImpure() {
return Math.floor(Math.random() * 6) + 1;
}
// Pure: the randomness arrives as an argument
function rollPure(unit) {
return Math.floor(unit * 6) + 1;
}
console.log(rollPure(0), rollPure(0.5), rollPure(0.999));
console.log('pure is repeatable:', rollPure(0.5) === rollPure(0.5));
console.log('impure only checkable by range:', rollImpure() >= 1 && rollImpure() <= 6);Example explained
Line 1`rollImpure` reads `Math.random()`, an input that never appears in its parameter list, so two identical calls can disagree.
Line 2`rollPure(unit)` receives the random number as data, which is why `rollPure(0)` is always 1 and `rollPure(0.999)` is always 6 — the edges become testable.
Line 3The last line shows the weakest assertion a hidden input allows: a range check instead of an equality check.
Line 4Real code then writes `rollPure(Math.random())`, leaving exactly one unpredictable line in the program.
Important notes
`console.log` is technically a side effect, so a logging function is not strictly pure; the impurities worth hunting are the ones that change data other code reads.
`Object.freeze(obj)` turns stray property writes into no-ops, or a TypeError in strict mode and modules, which is a fast way to catch a function that mutates its arguments.
Common mistakes
Calling `list.push(item)` or `list.sort()` on a parameter and assuming it is local work — the parameter is the caller's array, so other code later sees the extra item or the new order.
Copying with `{ ...user }` and then editing `copy.tags` or `copy.address` — the spread is one level deep, so nested objects are still shared and the original mutates anyway.
Thinking `const order = { ... }` protects the object — `const` only fixes the binding, so a function can still assign `order.qty` and cause a side effect.
Reaching for `Date.now()` inside a formatting function — the same input renders differently at 23:59 and 00:01, so a test that passed yesterday fails today.
Try it yourself
Change, predict, then run
In a browser console, define `const user = { name: 'Ada', tags: ['admin'] }` and write `rename(user, newName)` that returns a new object with the new name. Log `user.name`, the returned object, and `Object.is(renamed, user)` to prove the original was untouched.
Open the JavaScript workspaceCheck your understanding
A function's entire body is `counts[key] = (counts[key] ?? 0) + 1; return counts;` and it mentions nothing besides its two parameters. Why is it still impure?
- Assigning to `counts[key]` changes the object the caller passed in, so the call is observable outside the function.
- `??` reads a value that may be `undefined`, and reading undefined values is nondeterministic.
- It returns an object; only functions that return primitives can be pure.
- It would be pure if the caller declared `counts` with `const` instead of `let`.
Show answer
Purity is about what the rest of the program can observe, and an object parameter is a reference to the caller's own data, so a property assignment is a change outside the function. Returning an object is not the problem — a function that builds a fresh object and returns it stays pure — and `const` at the call site changes nothing, since it fixes the binding rather than the object's contents.