Expert · Functional patterns and testing · topic 47 of 61 · 25 min
Pure functions and immutability
Explanation
Why functional style?
Many bugs come from data changing unexpectedly: a function deep inside the code changes an array that three other parts of the program are also using. Functional programming is a style that reduces this by preferring pure functions and data that does not change. You do not need a special language; JavaScript supports this style well.
Pure functions
A function is pure when (1) its result depends only on its arguments, and (2) it has no side effects. A side effect is anything a function does besides returning a value: changing a variable outside, changing its arguments, writing to a file or database, logging, reading the clock or Math.random.
// Impure: depends on and changes outside state
let total = 0;
function addToTotal(x) { total += x; return total; }
// Pure: same input → same output, nothing changed outside
function add(a, b) { return a + b; }
// Impure: result depends on the current time
function isWeekend() { return [0, 6].includes(new Date().getDay()); }
// Pure version: pass the date in
function isWeekendOn(date) { return [0, 6].includes(date.getDay()); }Real programs need side effects (saving orders, sending SMS). The goal is to push them to the edges: keep the core logic (prices, discounts, validation) pure, and do the I/O in a thin outer layer. The pure core is then trivial to test.
Immutable updates
Immutability means you never change an existing object or array. To "change" something, you make a new version with the change, leaving the old one untouched. Spread, map, filter, slice, toSorted and with all return new values.
const cart = [{ sku: 'A', qty: 1 }, { sku: 'B', qty: 2 }];
const added = [...cart, { sku: 'C', qty: 1 }]; // add
const removed = cart.filter((item) => item.sku !== 'A'); // remove
const updated = cart.map((item) => item.sku === 'B' ? { ...item, qty: 5 } : item); // change one
const sorted = cart.toSorted((a, b) => b.qty - a.qty); // sort without mutating
console.log(cart); // still the original two items, untouched| Mutating (avoid in shared data) | Immutable alternative |
|---|---|
| arr.push(x) | [...arr, x] |
| arr.splice(i, 1) | arr.filter((_, j) => j !== i) |
| arr[i] = x | arr.with(i, x) or arr.map(...) |
| arr.sort() | arr.toSorted() |
| obj.key = v | { ...obj, key: v } |
| delete obj.key | const { key, ...rest } = obj |
Why this helps
- Easier debugging: data only changes where you create new versions, so you can follow it.
- Easy "undo" and history: keep the old versions.
- Cheap change detection: if oldState !== newState, something changed. React, Redux and many state libraries rely on this.
- Safe sharing: no function can surprise another by changing shared data.
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
Pure functions and immutability · 1/6
In this lesson you will write pure functions and update data without changing it.
Prefer a video by a teacher? Find free videos on “javascript pure functions immutability functional programming” on YouTube ↗
In real life: Undo in a billing screen
A restaurant POS app lets the cashier undo the last change to a bill. Because every edit creates a new bill object instead of changing the old one, the app simply keeps a list of previous bills. Undo means showing the previous version.
const history = [{ items: [] }];
const current = () => history.at(-1);
const addItem = (bill, item) => ({ ...bill, items: [...bill.items, item] });
history.push(addItem(current(), 'Paneer tikka'));
history.push(addItem(current(), 'Naan'));
history.pop(); // undo
console.log(current().items); // ['Paneer tikka']Practice (0/3 done)
Make addTax pure: it should not change the global taxTotal, just return amount plus 18% tax, rounded.
Write setQty(cart, sku, qty) that returns a NEW cart where the item with that sku has the new qty; items with qty 0 are removed. The original cart and its item objects must not change.
Write moveCity(state, userId, city) for state = { users: [{ id, name, address: { city } }] }. Return a new state where that user's address.city is changed. Every object on the path must be copied; untouched users must keep the SAME object (so change detection works).
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Composition, currying and pipelinesFunctional patterns and testing · same module
- Memoization, debounce and throttleFunctional patterns and testing · same module
- Testing your codeFunctional patterns and testing · same module
- Scope, hoisting and closuresFrontend Web Development · JavaScript in depth
- Abstract classes, interfaces and good class designC++ Programming (modern C++ and DSA) · Operator overloading, inheritance and polymorphism
- Lambdas and functional interfacesJava Programming (Core to Spring Boot) · Lambdas, streams and files
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
Which function is pure?
Answer: (a) => a * 2
Only a * 2 depends only on its input and changes nothing.
Which is an immutable way to add an item?
Answer: [...list, x]
Spread creates a new array.
Why do React and Redux like immutable updates?
Answer: A changed reference cheaply shows that something changed
Comparing references is fast change detection.
const b = { ...a }; b.inner.x = 1; Does a.inner.x change?
Answer: Yes, inner is shared
Spread is shallow.
Which function is pure?Related
Answer: One that returns a * b
a * b depends only on its inputs and changes nothing outside.
Which of these is a pure function?Related
Answer: (n) => n * 2
n * 2 always gives the same output for the same input and changes nothing. The others change an outer variable, print (a side effect) or give random results.
Why do interfaces make testing easier?Related
Answer: You can pass a fake implementation instead of a real network or database one
Code that depends on an interface can be tested with a fake or in-memory implementation.
What is a functional interface?Related
Answer: An interface with exactly one abstract method
Lambdas implement functional interfaces.
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- 4 more topics in “Functional patterns and testing” before its exam: Composition, currying and pipelines, Memoization, debounce and throttle, Testing your code, Test doubles and testable design.
- Module 10 exam: Functional patterns and testing · 12 questions · 25 min, without help. 5 topics of this module still to finish first.
- Then the next module, Performance and Node.js. 61 topics left in the course before the final exam.