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.
- 01Say what pure meansExplain the two rules: the same inputs give the same answer, and nothing outside changes.
- 02Spot side effectsFind the time, random numbers, outside variables, console messages, network calls, and changed arguments that make a function impure.
- 03Split code for testsMove the calculation into a pure function, and pass in the time, random numbers, and data as arguments.
Same input, same output, no surprises
A pure function is a function you can trust, like a pocket calculator. It follows two rules:
- Give it the same inputs, and it gives back the same answer. Every time.
- It changes nothing outside itself. Nobody outside the function can tell it ran, except by its answer.
The inputs are the values you pass in the brackets, called arguments. The answer is the value the function gives back with return, called the return value.
Pure functions are not fancy. This one is pure:
function add(a, b) {
return a + b;
}add(2, 3) always gives 5. Calling it does not print anything, save anything, or change anything. It also does not read anything that can change, like the clock.
You learned how to write functions in the functions basics lesson. Pure functions matter most when you pass them to array methods like map, filter, and reduce.
A good vending machine gives the same snack for the same coins and button. It does not turn off the lights. It does not move your chair. It does not take extra coins from your pocket.
A strange machine might still give you the snack. But it also flips the lights, or checks the clock, or adds one to a counter behind the wall. That machine is like an impure function.
- In real life: The same coins and the same button
- In JavaScript: The same arguments
- In real life: The same snack every time
- In JavaScript: The same return value
- In real life: Nothing else in the room changes
- In JavaScript: No side effects (no changes outside the function)
- In real life: A strange machine that also flips the lights
- In JavaScript: An impure function that changes things outside itself
function double(price) {
return price * 2;
}
console.log(double(4));
console.log(double(4));Line 1 says double takes one input, price. Line 2 doubles it and returns the result. Lines 5 and 6 call double(4) twice. Both lines print 8.
The function reads nothing except price. It does not read the clock, a random number, or a variable from outside. It does not change anything either.
double uses only the number you give it. Give it 4 and you get 8, every time.A function is pure when two things are true:
- Its answer depends only on its inputs.
- It returns that answer and changes nothing that the rest of the program can see.
Here is what comes next in this lesson:
- You will find the hidden changes that make a function impure.
- You will replace a pure call with its answer and see that nothing changes.
- You will split a messy checkout function into a pure part and an outside part.
- You will see why pure functions make tests simple.
Side effects: changes the outside world can notice
INTERACTIVEA side effect is anything a function does besides returning a value, that someone outside can notice. Common side effects in JavaScript:
- Changing an argument, an object, an array, or a variable outside the function.
- Writing somewhere:
console.log, the page, browser storage, a file, or the network. - Reading something that changes on its own:
Date.now()(the current time),Math.random()(a random number), a variable outside the function, the page address, or a form field. - Starting a timer, sending an event, or changing the browser's back-button history.
Reading the time counts. Reading a random number counts too. A function that only reads can still be impure, because its answer no longer depends only on its arguments.
console.log counts as well. It puts a new message in the console, and anyone can see that message.
let taxRate = 0.1;
function totalWithGlobal(price) {
return price + price * taxRate;
}
function totalWithArgument(price, tax) {
return price + price * tax;
}
console.log(totalWithGlobal(10));
taxRate = 0.2;
console.log(totalWithGlobal(10));
console.log(totalWithArgument(10, 0.1));totalWithGlobal reads taxRate, a variable outside the function. Line 11 prints 11. Line 12 changes taxRate to 0.2. Now line 13 makes the very same call, but prints 12.
totalWithArgument gets the tax in its brackets instead. Line 14 passes 0.1, so it prints 11 again. Date.now() and Math.random() are hidden inputs in the same way as taxRate.
taxRate, outside the function, so the same call gave 11 and then 12. On the right, the tax comes in the brackets, so the answer is always 11.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: The same input gives the same answer, and nothing outside changes.
Date.now() and Math.random() on a click or after the page loads.The checker calls each small function twice with the same input. Look at two things:
- Did the answer change? Then the function is impure.
- Did something outside change, even with the same answer? Then the function is impure too.
So purity is not only about the return value. It is about everything the outside world can notice.
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)}`;
Put each line of code into Pure or Impure. Read the explanation, even when you get it right.
Some sites build the page's HTML on the server first. Then the browser runs the same code again to make the page interactive. This second step is called hydration, and both runs must produce the same HTML.
So don't call Date.now() or Math.random() while building that HTML. The server and the browser would get different values. Read them when the user clicks, after the page has loaded, or in outside code that passes fixed values in.
Replace a pure call with its answer
STEP THROUGHYou can replace a pure call with its answer, and the program still does the same thing. The long name for this is referential transparency.
For example, add(2, 3) is pure and gives 5. So anywhere you see add(2, 3), you could write 5 instead. The program would behave the same.
A recipe card for tomato soup always makes the same bowl of soup. If you only care about the soup, you can put the bowl on the table instead of the card. A pure call is like that: its answer can stand in for it.
- In real life: The recipe card for tomato soup
- In JavaScript: A pure call like
add(2, 3) - In real life: The bowl of soup
- In JavaScript: The return value,
5 - In real life: Putting the bowl on the table instead of the card
- In JavaScript: Writing the value instead of the call
You can swap a pure call for its answer. 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 read. You can understand one line by looking only at that line.
The impure counter is different. To know what nextCount() gives, you must ask:
- How many times has it run already?
- Did any other code change
counter?
Once the answer depends on questions like these, you can't replace the call with one fixed value.
Move side effects out: a pure core and an outer shell
STEP THROUGHReal apps need side effects. A checkout page has to:
- read the cart;
- know the current time;
- maybe pick a random discount;
- save the order;
- update the screen.
So the goal is not to remove side effects. The goal is to move them to the edges of your code. Then the middle is a plain calculation.
Two names describe this split:
- The shell is the outer code. It does the side effects: it reads the page, the time, and random numbers, and it saves and shows results.
- The core is the inner code. It is pure. It gets plain values as arguments and returns a plain value.
In a good restaurant, the cooks don't answer the phone, seat guests, and take payments. The front desk does those jobs. The kitchen gets a clear order ticket and cooks the meal. Your code can split its work the same way.
- In real life: The front desk talks to customers and watches the clock
- In JavaScript: The shell: the page, time, random numbers, network
- In real life: The kitchen cooks from the order ticket
- In JavaScript: The core: works out totals from its arguments
- In real life: The finished plate goes back to the front desk
- In JavaScript: The core returns a value to the shell
function cartTotal(cart, discount) {
const subtotal = cart.reduce(
(total, item) => total + item.price * item.quantity,
0,
);
return subtotal - discount;
}
const cart = [
{ name: "Notebook", price: 12, quantity: 2 },
{ name: "Pen", price: 3, quantity: 3 },
];
console.log(cartTotal(cart, 5));reduce goes through the cart one item at a time and keeps a running total. For each item, it adds price times quantity. That gives 24 for the notebooks and 9 for the pens.
The cart and the discount both come in as arguments. The total is 33, minus the discount of 5. So line 13 prints 28, every time. A shell can read the real cart and pass it in. This function never has to reach outside.
Step through the original checkout. Notice each place where 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());Compare the two versions in the replay. The first one reaches outside for the cart, the time, and a random number. The second one, summarize, gets all three as arguments:
cart: the items to add up.now: the current time, as text.rng: a function that gives a random number. The name is short for "random number generator".
In a test, you pass a fixed time and an rng that always gives the same number. In the real app, the shell passes the real time and Math.random.
summarize only calculates and returns a plain value.Does a calculation need the time, a random number, a setting, or data from the browser? Take it as an argument. The function still uses those things. But now you can see exactly what goes in.
Pure functions are easy to test
INTERACTIVEA test is a small program that calls your code and checks the answer. A good test gives the same result every time it runs. Pure functions make that easy:
- You pass in known inputs.
- You check the return value.
- The test doesn't care what time it is, or which test ran before it.
An impure function is harder. The test may need fake timers, fake random numbers, a fake page, or a fake network. It may also need to reset outside variables after each run.
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 test passes every run, because the time and the random number are fixed arguments.
The lab pretends to run two tests. The pure test uses a fixed random number, so it passes every time. The other test gets a different random number on each run, so its two totals don't match. A test that sometimes passes and sometimes fails is called flaky.
The same thing happens in real projects when code reads Date.now(), Math.random(), a saved value in another file, browser storage, or the page. Pass the changing value into a pure core instead. Then the test proves the behavior every time.
function cartTotal(cart, discount) {
return cart.reduce(
(total, item) => total + item.price * item.quantity,
0,
) - discount;
}
function handleCheckout(cart) {
const total = cartTotal(cart, 5);
console.log("save order", total);
}
handleCheckout([{ price: 10, quantity: 2 }]);cartTotal only calculates. handleCheckout is the shell: it calls cartTotal and then writes to the console. Line 13 calls it, and the console shows save order 15.
In a real app, handleCheckout could also read a form or save the order to a server. It would do that after the pure calculation is finished.
function formatPrice(amount) {
return "$" + amount.toFixed(2);
}
console.log(formatPrice(3));
console.log(formatPrice(3));Both calls print $3.00. The answer depends only on amount.
So you could save the first answer and reuse it the next time amount is 3. Saving answers to reuse them is called memoization, or caching. Never cache a function that secretly reads the time or a random number. The saved answer could be wrong later.
Changing an argument is a side effect
INTERACTIVETo mutate means to change an object or array that already exists.
The Objects & references lesson showed that passing an object or array to a function does not copy it. The caller and the function share the same one.
So if a function calls push or sort on an array it was given, the caller's array changes. The same happens if it sets a property, like user.name = "Ada", on an object it was given.
function addLabel(user, label) {
user.labels.push(label);
return user.labels.length;
}
const user = { labels: ["js"] };
console.log(addLabel(user, "pure"));
console.log(user.labels.join(","));Line 2 pushes "pure" into user.labels. So line 7 prints 2, the new length. Line 8 reads the caller's own user and prints js,pure. The caller's object changed.
If addLabel returned a new object instead, the caller's user would stay as it was.
addLabel hold the same user object, not two copies. So when addLabel pushes "pure", the caller sees it too.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 changes the array and returns that same array. The spread version, [...todos, text], returns a new array, so it doesn't touch the caller's list.
The pure way is to return a new value and leave the old one alone:
[...items, item]makes a new array with one more item.items.toSorted()returns a sorted copy, in browsers that support it.{ ...user, name }makes a new object with a newname.- For objects inside objects, copy each level you change.
Copying takes some work, and the next lesson, on immutability, goes deeper. The rule is simple: for a pure function, don't change data that other code can still see.
Where you'll use this
Pure functions fit code that turns data into other data. These are often pure:
- a search filter;
- a shopping-cart total;
- a form check, like "is this email valid?";
- a function that builds a web address;
- a function that formats a price or a date.
Then the code that runs on a click becomes a small shell. It reads the input, calls the pure function, and shows the result.
The table compares the two kinds of function. Each row asks one question; read across to see how each kind answers it.
| Question | Pure function | Impure function |
|---|---|---|
| Where does it get its data? | Only from its arguments. | Also from outside: variables, the time, random numbers, the page, storage, or the network. |
| What does it do with its answer? | It returns the answer. That is all. | It may return an answer and also change something somewhere else. |
| What if you call it twice the same way? | You get the same answer, and nothing else changes. | The answer may differ, or the first call may change what the second call does. |
| How do you test it? | Pass in fixed inputs and check the return value. | Fake the outside things (time, page, network), or reset them after each test. |
| Is it safe to save and reuse its answers? | Yes, if you save each answer under all of its inputs. | No. A saved answer can be wrong later, when a hidden outside value changes. |
Array methods from map, filter & reduce work best when the function you pass in is pure.
Closures need care. A closure is a function that remembers variables from where it was made. If it remembers a variable that keeps changing, it is impure. If it remembers a value that never changes, like a tax rate set once, it can still be pure.
A cache saves an answer and gives it back the next time the same inputs come in. That is only safe for a pure function. If the function secretly reads the clock or changes an outside variable, the cache can give back an old, wrong answer. A later lesson covers caching in depth.
Common misconceptions
"A pure function can't have variables inside."
Variables inside the function are fine. A pure function can make arrays, objects, and loops inside itself. It only becomes impure when its answer depends on hidden outside values, or when it changes something outside.
"If the return value is the same, it is pure."
Not always. A function that calls console.log(value) may return the same value every time, but it still writes to the console. A function that changes its argument can also return the same value, while it changes the caller's data.
"Reading is always safe."
Reading an argument is safe. Reading something that changes on its own is not. That includes Date.now(), Math.random(), a variable outside the function, the page, browser storage, and the network.
"Pure functions are not useful in real apps."
Real apps need side effects, but not everywhere. Put the side effects in a shell, and keep the calculations pure. Then the app does real work, and its logic is still predictable.
"Copying data makes a function pure."
Copying helps, but it is not enough on its own. A copied array can still hold the same inner objects as the original. And the function could still read the clock or log a message. Purity covers everything the function does.
Practice: make behavior predictable
5 EXERCISESWork out what this prints. 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. It is pure.
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 and returns 1. The second call changes it from 1 to 2 and returns 2. So the lines are 1 and 2. Changing calls is the side effect.
A greeting that reads the clock is impure. Rewrite it so the function takes hour as an argument. What does it print 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. The pure function gets it as the hour argument. With name set to Ada and hour set to 9, it prints Good morning, Ada.
The function sorts a copy. 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] makes a new array. Sorting the copy leaves original as pear,apple. sorted is apple,pear.
Pick a feature from a site you know: search, cart totals, form checks, or sorting. Write down which parts are side effects for the shell. Then write down which part could be a pure core.
Take a search box. The shell reads the text in the search box and updates the page. The pure core could be filterProducts(products, query), which returns a new array. You can test it with fixed products and fixed search words.
Quiz: check your understanding
7 QUESTIONSChoose an answer, then read the explanations. The wrong answers explain common mistakes.
Question 1 of 7Which sentence describes 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 has a side effect?
Choose an answer to see the explanation.
Question 4 of 7You replace a pure call with its answer, and the program still does the same thing. What is this called?
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 does the checkout function take
nowandrngas arguments?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 gives the same answer for the same inputs, and changes nothing outside itself.
- Side effects include changing outside data, writing (console, page, storage, network), and reading things that change, like the time, random numbers, or outside variables.
- You can replace a pure call with its answer, and the program still does the same thing.
- Keep the calculation in a pure core. Keep the clock, random numbers, page, storage, and network in a shell.
- Pure functions are easy to test, because everything they use comes in as an argument.
Remember the one-liner.
Pure functions work out answers from their inputs; side effects belong at the edges.
Up next: Immutability, which means changing data by making copies instead of changing the original.