Command Palette

Search for a command to run...

Hectal
PHASE 3Beginner ~26 min· topic 9 of 9

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.

tax.jswhole filejavascript
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 output

02Make 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.

tax.jswhole filejavascript
function total(price, rate) {
  return price + price * rate;
}
total(200, 0.1); // always 220

03Don't mutate what you're given

push changes the caller's array. Spreading into a new array leaves the original alone.

cart.jswhole filejavascript
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.

Push side effects to the edgediagram
Rendering diagram…

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.

iife.jswhole filejavascript
const counter = (function () {
  let count = 0;          // private
  return {
    next: function () { count += 1; return count; },
  };
})();
counter.next(); // 1

Try it yourself

  1. 1

    Purify a function

    In the first example, rewrite countedDouble so it's pure. What must you give up? (Hint: the counting has to move to the caller.)

  2. 2

    Make an immutable remove

    In the second example, write removeItem(cart, item) that returns a new array without item, using filter. Log myCart afterwards to prove it didn't change.

  3. 3

    Test the pure core

    In the third example, add three console.log lines that call orderTotal with different inputs and predict each result first. You didn't need the page at all.

Code & diagrams

Pure and impure, side by side New tab

Console

(nothing was printed)

Expected console output

220
240
220 220
countedDouble ran 2 times
Mutating an argument vs returning a copy New tab

`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 ]
A pure core with a thin impure shell New tab

Only `update` touches the page. The two pure functions can be tested with plain calls and compared results.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

750 675
₹675.00
The IIFE, a historical pattern New tab

Before `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 undefined

Break 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.

terminal
$ console.log(original)
── what you'll see ──
The original array is now sorted too.

Break #2

IIFE after a line without a semicolon

Write const a = 1 (no semicolon) and start the next line with (function () { ... })();.

terminal
$ Console
── what you'll see ──
Uncaught TypeError: 1 is not a 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=iife produces 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. 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's area(width, height) is pure; its partner showArea is not.

  2. 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 to localStorage, logging, starting a timer, calling fetch. Reading Date.now() or Math.random() makes a function impure too, because the output changes without the inputs changing.

  3. 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. 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's cart too. To stay pure, build and return a new array or object instead: return [...cart, item]; or return { ...user, name: newName }; (spread, Topic 3.3). The newer array methods toSorted, toReversed and with (ES2023) return changed copies instead of mutating.

  5. 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). Today let/const blocks 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 use await where top-level await isn't available.

Explain it without notes

01

Define a pure function and give one example of a pure and one of an impure function.

02

List five kinds of side effects a JavaScript function can have.

03

Why can a function change an array passed to it, and how do you avoid it?

04

What is an IIFE, why was it widely used, and what replaces it today?

Practice

01

Make this function pure: let count = 0; function next() { count++; return count; } (hint: take the current count as a parameter).

02

Write a pure applyDiscount(product, percent) that returns a new product object with a reduced price, leaving the original untouched.

03

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.