Topic 3.9
Pure Functions and Side Effects
In one line
A pure function always returns the same output for the same inputs and changes nothing outside itself; everything else a function does to the outside world is a side effect, and the IIFE is an older pattern that wrapped code in a function just to keep its variables private.
Think of it like this
A calculator versus a vending machine. Press 2 + 3 on a calculator and you get 5, today, tomorrow, on anyone's calculator, and nothing else in the world changes: that's a pure function. A vending machine also gives you something for your coin, but it changes the world as it does: its stock goes down, your money goes in, and when it's empty the same coin gives a different result. Those changes are side effects.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Pure function
- A function whose result depends only on its inputs and which changes nothing outside itself.
- Side effect
- Any change a function makes outside itself, like updating a variable, the page, storage or the console.
- Impure
- Not pure: it reads changing outside state or causes side effects.
- Mutate
- Change an existing object or array in place, instead of making a new one.
- Reference
- A link to an object. Passing an array to a function passes a link to the same array, not a copy.
- Immutable update
- Making a changed copy (like
[...list, item]) instead of changing the original. - IIFE
- Immediately Invoked Function Expression:
(function () { ... })();, a function run as soon as it's defined. - Module pattern
- An older trick: an IIFE that returns an object of functions sharing private variables.
Step by step
01Spot what the result depends on
impureTotal reads taxRate from outside. Change that variable and the same call gives a different answer.
let taxRate = 0.1;
function impureTotal(price) {
return price + price * taxRate; // depends on outside state
}
impureTotal(200); // 220
taxRate = 0.2;
impureTotal(200); // 240: same input, new output02Make it pure by passing everything in
Turn the hidden input into a parameter. Now the output depends only on the arguments, so the function is trivial to test.
function total(price, rate) {
return price + price * rate;
}
total(200, 0.1); // always 22003Don't mutate what you're given
push changes the caller's array. Spreading into a new array leaves the original alone.
function addItemBad(cart, item) {
cart.push(item); // side effect on the caller's array
return cart;
}
function addItem(cart, item) {
return [...cart, item]; // new array, original untouched
}04Push side effects to the edge
Keep calculations pure and do the reading and writing in one small outer function.
05Recognise an IIFE
Wrap a function in parentheses and call it straight away. Its variables stay private, and only what it returns escapes. Today a block or a module does the same job more simply.
const counter = (function () {
let count = 0; // private
return {
next: function () { count += 1; return count; },
};
})();
counter.next(); // 1Try it yourself
- 1
Purify a function
In the first example, rewrite
countedDoubleso it's pure. What must you give up? (Hint: the counting has to move to the caller.) - 2
Make an immutable remove
In the second example, write
removeItem(cart, item)that returns a new array withoutitem, usingfilter. LogmyCartafterwards to prove it didn't change. - 3
Test the pure core
In the third example, add three
console.loglines that callorderTotalwith different inputs and predict each result first. You didn't need the page at all.
Code & diagrams
Console
(nothing was printed)Expected console output
220
240
220 220
countedDouble ran 2 times`addItemBad` changed the caller's array (`myCart` now has milk). `toSorted` (ES2023) sorts a copy; `sort` would change `scores` itself.
Console
(nothing was printed)Expected console output
[ 'apple' ] [ 'apple', 'bread' ]
[ 'apple', 'milk' ] [ 'apple', 'milk' ] true
{ name: 'Asha', city: 'Pune' } { name: 'Asha', city: 'Delhi' }
[ 10, 20, 30 ] [ 30, 10, 20 ]Only `update` touches the page. The two pure functions can be tested with plain calls and compared results.
Preview
Console
Nothing logged yet.Expected console output
750 675
₹675.00Before `let`, `const` and modules, an IIFE was the only way to hide variables. A block with `const` now does it without a function.
Console
(nothing was printed)Expected console output
1 2
undefined
arrow IIFE
a block does the same job today
undefined undefinedBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
A helper that secretly changes your data
Write function sortedCopy(list) { return list.sort(); } and call it on your array.
Break #2
IIFE after a line without a semicolon
Write const a = 1 (no semicolon) and start the next line with (function () { ... })();.
Myth vs fact
Myth
Side effects are bad and should be avoided.
Fact
Every useful program has side effects. The aim is to keep them in a few clear places and keep the calculations pure.
Myth
Pure functions can't use variables, loops or arrays inside.
Fact
Purity is about what's visible from outside. A pure function may create and change its own local variables and arrays freely; it only mustn't change anything that existed before the call.
Myth
const makes an array immutable, so functions can't change it.
Fact
const only stops the variable being reassigned. The array itself can still be pushed to and changed through any reference.
Myth
IIFEs are needed to make variables private.
Fact
That was true before ES2015. Today block scope ({ const x }) and ES modules give privacy without an extra function.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Purity enables referential transparency: a call can be replaced by its result without changing the program, which is what makes memoisation, React's re-rendering model and compiler optimisations safe.
- ▸
structuredClone(obj)(in all modern browsers since 2022) makes a deep copy; spread copies only one level, so nested objects are still shared. Immutable updates of deep data need spreading at every level you change. - ▸
Bundlers wrap code in IIFE-like functions to isolate modules, and libraries published as
<script>builds (UMD/IIFE formats) still use them.esbuild --format=iifeproduces exactly this pattern. - ▸
Exceptions are also an effect: a function that throws for bad input isn't strictly "total". Functional styles return result objects (
{ ok, value, error }) instead, though throwing is idiomatic JavaScript.
Remember this
- 1
A function is pure when two rules hold. (1) Same inputs, same output: its result depends only on its arguments, not on outside variables, the time, random numbers or what's on the page. (2) No side effects: it doesn't change anything outside itself: no outside variables, no changes to objects or arrays passed in, no DOM updates, no
console.log, no storage, no network requests.const add = (a, b) => a + b;is pure. Topic 3.1'sarea(width, height)is pure; its partnershowAreais not. - 2
A side effect is any change a function makes that can be seen outside it, or any reliance on outside state that can change. Examples: assigning to an outer variable, pushing to an array that was passed in, setting
textContent, writing tolocalStorage, logging, starting a timer, callingfetch. ReadingDate.now()orMath.random()makes a function impure too, because the output changes without the inputs changing. - 3
Side effects aren't bad: they're the whole point of a program. Showing results, saving data and talking to servers are all side effects. The goal is to keep them in a few known places. A good design is a pure core (functions that calculate: prices, validation, formatting) with a thin impure shell around it (reading inputs, updating the page). Pure functions are easy to test (call, compare the result), easy to reuse, safe to cache (memoisation from Topic 3.8 only works for pure functions), and easy to reason about because nothing else can change their answer.
- 4
The sneakiest side effect is mutating an argument. Objects and arrays are passed as references: the function receives a link to the same array the caller has, not a copy. So
function addItem(cart, item) { cart.push(item); }changes the caller'scarttoo. To stay pure, build and return a new array or object instead:return [...cart, item];orreturn { ...user, name: newName };(spread, Topic 3.3). The newer array methodstoSorted,toReversedandwith(ES2023) return changed copies instead of mutating. - 5
An IIFE (Immediately Invoked Function Expression, said "iffy") is a function that's defined and called in one go:
(function () { ... })();. The first parentheses turn the function into an expression; the last()call it at once. Before ES2015 there was no block scope and no modules, so wrapping a whole script in an IIFE was the way to keep its variables out of the global scope and to create private state (combined with closures this was the module pattern). Todaylet/constblocks and ES modules do that job, so IIFEs are mostly historical. You'll still meet them in older code, in bundler output, and as(async () => { ... })()to useawaitwhere top-level await isn't available.
Explain it without notes
Define a pure function and give one example of a pure and one of an impure function.
List five kinds of side effects a JavaScript function can have.
Why can a function change an array passed to it, and how do you avoid it?
What is an IIFE, why was it widely used, and what replaces it today?
Practice
Make this function pure: let count = 0; function next() { count++; return count; } (hint: take the current count as a parameter).
Write a pure applyDiscount(product, percent) that returns a new product object with a reduced price, leaving the original untouched.
Rewrite this IIFE with a block and const: (function () { var secret = 42; console.log(secret); })();
Trade-offs
- ↔
Returning copies instead of mutating makes changes easy to track (and undo), but copies large arrays and objects on every change; for big data in hot loops, careful local mutation is faster.
- ↔
Pure functions need all their inputs passed in, which can mean longer parameter lists; an options object (Topic 3.3) keeps that tidy.
Done when you can
I can tell whether a function is pure.
I can list common side effects, including mutating arguments.
I can update arrays and objects without mutating them.
I can split code into a pure core and an impure shell.
I can read an IIFE and say what replaces it in modern code.