Pure functions & side effects
Learn how pure functions keep JavaScript predictable: same input, same output, no hidden changes, and side effects isolated at the edges.
- 01Define purityExplain same input, same output, with no observable side effects.
- 02Spot impurityClassify time, randomness, globals, logging, network calls, and mutation.
- 03Refactor for testsExtract a pure core and inject clocks, randomness, and data.
Same input, same output, no surprises
A pure function is a function you can trust like a small calculator: give it the same inputs, and it gives the same output, while changing nothing observable outside itself. No hidden writes. No secret clock reading. No random surprise. No mutation of an argument the caller still owns.
That does not mean pure functions are fancy or academic. This one is pure:
function add(a, b) {
return a + b;
}The important part is not the word function. It is the contract: add(2, 3) is always 5, and calling it does not log, save, mutate, send, draw, or read changing state. The idea is closely related to the function basics lesson, and it becomes powerful when combined with higher-order functions like map, filter, and reduce.
Imagine a vending machine that always gives the same snack for the same coins and button, and does not change the lights, move your chair, or take extra coins from your pocket. That is purity. An impure machine might still give the snack, but also flip the lights, check the clock, or change a counter behind the wall.
- In real life: Same coins and button
- In JavaScript: Same arguments
- In real life: Same snack every time
- In JavaScript: Same return value
- In real life: Nothing else in the room changes
- In JavaScript: No observable side effects
- In real life: A suspicious machine that flips lights
- In JavaScript: An impure function that changes outside state
Where the analogy stops: Real vending machines can run out of snacks. A pure function has no hidden inventory; if stock matters, pass it in and return the new stock as data.
A pure function's observable behavior depends only on its inputs. It returns a value and does not perform observable side effects.
The rest of the lesson is about making that definition useful. You will detect side effects, replace pure calls with values, refactor a messy checkout calculation into a pure core with an imperative shell, and see why tests become calmer when code is pure.
Side effects: the outside world noticing
INTERACTIVEA side effect is an observable interaction besides returning a value. In JavaScript, common side effects include:
- mutating an argument, object, array, or outer variable;
- writing I/O such as
console.log, the DOM, storage, a file, or the network; - reading changing state such as
Date.now(),Math.random(), a global variable, the current URL, or form fields; - starting timers, dispatching events, or changing browser history.
Notice that reading time and randomness counts. A function can be impure even if it never writes anything, because its result no longer depends only on the arguments. Also notice that console.log is technically a side effect: the outside world can observe a new console message.
let counter = 0; function addTax(price) { return price * 1.1;} function stampedTotal(price) { return price + Date.now();} function luckyDiscount(price) { return price - Math.random();} function countCalls(price) { counter += 1; return price + counter;} function announce(price) { console.log("checking", price); return price;} function addItem(cart, item) { cart.push(item); return cart.length;}1111nothing changedPure: Same input, same return value, and no observable outside change.
Date.now() and Math.random() should run on click or after mount, not during render.The detector runs each tiny function twice with the same apparent input. Sometimes the return value changes. Sometimes the return value stays the same but the outside world changes, which still breaks purity. That is why purity is about observable behavior, not just “does it return the same thing in this one run?”
return a + b;return Date.now();return Math.random();console.log(value);return [...items].sort();items.push(value);return price + taxRate;return `$${amount.toFixed(2)}`;
Sort each card by the behavior it describes. Read the explanation even when you get it right.
In a server-rendered UI, never call Date.now() or Math.random() while rendering markup that must hydrate in the browser. Compute those values on a click, after mount, or in a shell that passes fixed values into a pure render calculation.
Referential transparency: swap the recipe card for the dish
STEP THROUGHA pure expression is often referentially transparent: you can replace the expression with its value and the program means the same thing. If add(2, 3) is pure, then wherever you see it, writing 5 should not change the behavior.
If a recipe card has already produced a bowl of soup, and nobody cares about the act of cooking, you can talk about the bowl instead of the card. Pure calls are like that: the returned value fully represents the call.
- In real life: Recipe card for tomato soup
- In JavaScript: A pure call like
add(2, 3) - In real life: The bowl of soup
- In JavaScript: The returned value,
5 - In real life: Replacing the card on the table with the bowl
- In JavaScript: Replacing the expression with its value
Where the analogy stops: Recipes take time and ingredients in real life. Referential transparency talks about program meaning, not whether computing the value is expensive.
A pure call can be replaced with its returned value. Step through the two versions.
script
function add(a, b) { return a + b;} const replaced = 5 * 5;console.log(total);console.log(replaced);This is why pure functions are easy to reason about. You can read code locally. The impure counter version needs history: how many times has this already run? Who else can change the counter? Once the answer depends on those questions, replacing a call with a value is no longer safe.
Extracting impurity: pure core, imperative shell
STEP THROUGHReal apps need side effects. A checkout page must read a cart, show the current time, maybe generate a discount, save an order, and update the screen. The goal is not to ban side effects. The goal is to move them to the edges so the center is a plain calculation.
A good restaurant does not let every cook answer the phone, seat guests, check the clock, and charge cards. The front desk handles the outside world. The kitchen receives a clear ticket and prepares the meal. Programs can use the same split.
- In real life: Front desk talks to customers and clocks
- In JavaScript: Imperative shell: DOM, time, randomness, network
- In real life: Kitchen follows the order ticket
- In JavaScript: Pure core: calculate totals from explicit inputs
- In real life: Finished plate goes back to the desk
- In JavaScript: Pure return value goes back to effectful code
Where the analogy stops: Real kitchens can be noisy and stateful. The analogy is about architecture: keep messy communication at the boundary and deterministic preparation in the center.
Step through the original checkout. Notice each place it reaches outside itself.
script
{ name: "Notebook", price: 12, quantity: 2 }, { name: "Pen", price: 3, quantity: 3 },]; function checkoutSummary() { const subtotal = globalCart.reduce( (sum, item) => sum + item.price * item.quantity, 0, ); const discount = Math.random() < 0.5 ? 5 : 0; return { subtotal, discount, placedAt: new Date(Date.now()).toISOString(), total: subtotal - discount, };} console.log(checkoutSummary());The refactor has two parts. The shell reads changing things: the real cart, the current time, a random source, storage, or the network. The core receives those values as arguments and returns a summary. In tests, you inject a fixed clock and a fixed random function. In production, the shell can pass the real ones.
If a calculation needs time, randomness, configuration, or data from a host API, accept those as parameters. You have not removed reality; you have made the boundary visible.
Testability: pure functions are boring in the best way
INTERACTIVETests want repeatability. A pure function gives it to them. You pass known inputs, assert the return value, and the test does not care what time it is or whether another test ran first. An impure function may require mocks, cleanups, fake timers, reset globals, browser setup, or network stubs.
const cart = [{ price: 10, quantity: 2 }];const fixedNow = "2026-09-25T09:00:00.000Z";const fixedRng = () => 0.25; summarize(cart, fixedNow, fixedRng);summarize(cart, fixedNow, fixedRng);15 and 15pass15 and 20fail this timeThe pure-core test passes every run because time and randomness are fixed inputs.
The lab shows a simulated flaky test with controlled randomness. In real projects, the same pattern appears when code reads Date.now(), Math.random(), a module-level cache, a mutable singleton, storage, or the DOM. Injecting the changing value into a pure core turns “sometimes fails” into “always proves the behavior.”
Mutation is a side effect when the caller can observe it
INTERACTIVEThe Objects & references lesson showed that arrays and objects are shared by reference. That matters here: if a function calls push, sort, or assigns object.name = ... on an object the caller passed in, the caller's world changed.
function addTodo(todos, text) { todos.push(text); return todos;} function addTodoPure(todos, text) { return [...todos, text];} const list = ["learn"];const changed = addTodo(list, "practice");const copied = addTodoPure(list, "quiz");console.log(list === changed);console.log(list === copied);[learn, practice]truefalse[learn, practice, quiz]push returns the same array after changing it. The spread version returns a new array, so the caller's original list stays separate.
The pure alternative is to return a new value: [...items, item], items.toSorted() when available, { ...user, name }, or a carefully cloned nested structure. Copying everything is not always free, and the next lesson on immutability goes deeper, but the principle is simple: if you want purity, do not mutate data someone else can still see.
Where you’ll use this
Pure functions are especially useful in code that transforms data. A search filter, shopping-cart total, validation rule, URL builder, reducer, or formatting function can often be pure. Then your UI event handler becomes a small shell: read input, call the pure function, write the result.
| Idea | Pure function | Impure function |
|---|---|---|
| Inputs | All important information is passed as arguments | Reads globals, time, random state, DOM, storage, or network |
| Outputs | Returns the answer | May return an answer and also change something elsewhere |
| Same call twice | Same observable result | May differ or alter later calls |
| Testing | Use fixed inputs and assert the return value | Mock, stub, click, wait, or reset outside state |
Functional array methods from map, filter & reduce are easier to use well when callbacks are pure. Closures can still be involved, but be careful: a closure over changing state can make a function impure. A closure over fixed configuration, like a tax rate passed once, can be fine.
Caching and memoization are safe only when the function is pure for the cache key. If a function secretly reads the clock or mutates a global, a cache can preserve the wrong answer. A later lesson covers memoization and laziness in depth.
Common misconceptions
“Pure means no variables inside.”
Local variables are fine. A function can create arrays, objects, loops, and helper values internally. It becomes impure when observable behavior depends on hidden state or changes something outside.
“If the return value is the same, it is pure.”
Not necessarily. console.log(value) might return the same value or no value, but it still writes to the console. Mutating an argument can also return a stable value while changing the caller's data.
“Reading is always safe.”
Reading a parameter is safe. Reading Date.now(), Math.random(), a mutable global, the DOM, storage, or the network reads changing outside state.
“Pure functions cannot be useful in real apps.”
Real apps need effects, but not everywhere. Put effects in a shell and keep calculations pure. That is how you get both useful behavior and predictable logic.
“Copying data is automatically pure.”
Copying helps, but a copied result can still include shared nested objects, and the function could still read the clock or log. Purity is the whole observable behavior.
Practice: make behavior predictable
5 EXERCISESWork out the output. Is the function pure?
function total(items) {
return items.reduce((sum, item) => sum + item.price, 0);
}
console.log(total([{ price: 2 }, { price: 3 }]));The function reads only its items argument. The prices are 2 and 3, so it prints 5.
Predict both printed lines, then name the side effect.
let calls = 0;
function countCalls() {
calls += 1;
return calls;
}
console.log(countCalls());
console.log(countCalls());The first call changes calls from 0 to 1. The second changes it from 1 to 2, so the logs are 1 and 2.
Rewrite a time-based greeting so the calculation accepts hour as an input. Check the output for Ada at 9.
function greeting(name, hour) {
return hour < 12 ? "Good morning, " + name : "Hello, " + name;
}
console.log(greeting("Ada", 9));function greeting(name, hour) {
return hour < 12 ? "Good morning, " + name : "Hello, " + name;
}
console.log(greeting("Ada", 9));The shell can read the real hour, but the pure core receives hour. With name Ada and hour 9, it prints Good morning, Ada.
Use a copy before sorting. What does the original array print?
function alphabetic(items) {
return [...items].sort();
}
const original = ["pear", "apple"];
const sorted = alphabetic(original);
console.log(original.join(","));
console.log(sorted.join(","));function alphabetic(items) {
return [...items].sort();
}
const original = ["pear", "apple"];
const sorted = alphabetic(original);
console.log(original.join(","));
console.log(sorted.join(","));[...items] creates a new array. Sorting that copy leaves original as pear,apple, while sorted becomes apple,pear.
Pick a feature from a site you know: search, cart totals, form validation, or sorting. Write down which parts are shell effects and which part could be a pure core.
For a search feature, the shell reads the input field and updates the page. The pure core might be filterProducts(products, query) returning a new array. You can test it with fixed products and queries.
Quiz: check your understanding
7 QUESTIONSChoose an answer, then read the explanations. The wrong answers are designed to teach.
Question 1 of 7Which description best defines a pure function?
Choose an answer to see the explanation.
Question 2 of 7What does this counter example print?
Read the code, then predictlet count = 0; function next() { count += 1; return count; } console.log(next()); console.log(next());Choose an answer to see the explanation.
Question 3 of 7Which line is a side effect?
Choose an answer to see the explanation.
Question 4 of 7What does replacing a pure call with its value mean?
Choose an answer to see the explanation.
Question 5 of 7What does this copy example print?
Read the code, then predictfunction addPure(list, item) { return [...list, item]; } const a = ["tea"]; const b = addPure(a, "cake"); console.log(a.length); console.log(b.length);Choose an answer to see the explanation.
Question 6 of 7Why inject
nowandrnginto a checkout calculation?Choose an answer to see the explanation.
Question 7 of 7When is
console.logpure?Choose an answer to see the explanation.
Key takeaways
- A pure function has the same observable output for the same inputs.
- Side effects include mutation, I/O, and reading changing state like time, randomness, or globals.
- Referential transparency means a call can be replaced by its value without changing behavior.
- Extract impurity by building a pure core and keeping clocks, randomness, DOM, storage, and network in a shell.
- Pure functions are easier to test because inputs and outputs are explicit.
Remember the one-liner.
Pure functions return answers from inputs; side effects belong at the edges.
Up next: Immutability, updating data by copying instead of mutating.