Functions: naming and reusing behaviour · Lesson 21 of 77 · practice

Pure functions and side effects

About 50 minutes.

By the end of this lesson you can

  • State the two conditions a function must meet to be called pure.
  • Spot the side effect in a small function.
  • Rewrite an impure function as a pure one by taking what it needs and returning what it produces.
  • Explain why pure functions are the easy ones to test.
  • Say honestly why a useful program cannot be pure all the way through.

Two questions to ask about any function

Explanation

First question: called twice with the same inputs, does it give the same answer both times? Second question: when it runs, does anything outside it change?

A function that answers yes to the first and no to the second is called pure. That is the entire definition, and it is worth learning as those two questions rather than as a slogan, because the questions are what you actually apply when reading code.

Purity is not a badge of quality and it is not all-or-nothing virtue. It is a property, like being sorted. Knowing which of your functions have it tells you which ones you can move, reuse, cache and test without thinking hard.

What counts as a side effect

Explanation

A side effect is any change a function makes to the world outside itself. The two you can already produce are assigning to a variable declared outside the function — lesson five's bug — and printing with console.log, which changes what is on the screen.

Later you will meet bigger ones: writing a file, sending a request over a network, changing a shared collection that somebody else is holding. They differ in scale, not in kind, and they all break the same promise: that calling the function does nothing except produce an answer.

Printing being a side effect surprises people, because it feels harmless. It is harmless in the sense that nothing breaks, and it is a side effect in the sense that matters: the function did something you cannot see from its return value, and running it twice is not the same as running it once.

What breaks the same-answer promise

Explanation

A function gives different answers to the same question when it reads something that changes underneath it. Three usual culprits: an outer variable that other code edits, the current time, and randomness.

Math.random is the clearest case. It takes no arguments and returns a different number nearly every time, by design — that is its entire job. Any function that calls it inherits that unpredictability, no matter how ordinary the rest of it looks.

This is not an argument against random numbers or clocks. Programs need them. It is an argument for knowing exactly which of your functions contain them, because those are the ones that cannot be tested by comparing against a fixed expected answer.

Why pure functions are the easy ones to test

Why it matters

Testing a pure function is: call it, compare the answer. No setup, no cleanup, no ordering, no worrying about what the last test left behind. That is the entire reason the practice is worth cultivating, and it applies to every test you will write in module eleven.

You have been on the receiving end of this all module. Every exercise here is graded by calling your function and comparing what came back — which only works because the contracts were written to be pure. The two tests you have met that check something else, that currentScore is untouched and that receipt is still empty, exist precisely because impurity is what they are guarding against.

The same property helps far beyond tests. A pure function can be moved to another file, called from anywhere, called twice, or skipped when its answer is already known, and none of that changes the behaviour of the program. Impure ones have to be handled carefully in all four cases.

Push the effects to the edges

A mental model

A program with no side effects does nothing observable. It cannot print, save, send or display. So the goal is never to eliminate effects — it is to be deliberate about where they live.

The shape to aim for: the calculation is pure, and a thin layer around it does the reading and the printing. Work out the total in a function that only takes numbers and gives back a number; print it in the line that calls that function. The interesting part is then testable, and the untestable part is small enough to read in one glance.

You can see this in your own exercises. safeDivide worked out an answer and returned it, and it was the surrounding code that printed. That was not decoration — it is what let five one-line tests pin the behaviour down completely.

The three impurities that show up first

A common mistake

Accumulating into an outer variable. The function works once, and every call after that starts from a value the previous call left behind. You met this in lesson five, and it is the single most common version.

Printing instead of returning. The value appears on screen, so the function looks finished, but the caller received nothing and the output cannot be tested against anything. Return the value; let the caller decide whether it deserves printing.

Reaching for the clock or the random number generator in the middle of a calculation. Take the value as a parameter instead, and let whoever calls you produce it. The calculation stays testable, and the unpredictable part moves to one visible place — which is also how the tests in module eleven get to control it.

Recap

Recap

A pure function gives the same answer for the same inputs and changes nothing outside itself. Assigning to outer variables and printing are both side effects; reading the clock, randomness or a variable that others edit breaks the same-answer half. Purity is not a moral position — a program with no effects does nothing — but keeping the calculations pure and the effects at the edges is what makes code that can be tested, moved and trusted.

Worked examples

Every output below was produced by actually running the code in the same sandbox your exercises run in — it is not a prediction.

The same sum, twice, two ways

Both functions add ten to a total. Call each of them twice with identical arguments and only one of them gives the same answer both times. The difference is where the running total lives.

1let cartTotal = 0;2 3function addImpure(price) {4  cartTotal = cartTotal + price;5  return cartTotal;6}7 8function addPure(runningTotal, price) {9  return runningTotal + price;10}11 12console.log(addImpure(10));13console.log(addImpure(10));14console.log(addPure(0, 10));15console.log(addPure(0, 10));
  • line 4

    The side effect. Something outside the function is different after this line runs, and nothing at the call site says so.

  • line 9

    The pure version takes the running total as a parameter instead of reaching for it, and returns a new number instead of changing one.

  • line 13

    Identical arguments, different answer: 20 rather than 10. This is the same-answer promise being broken, in one line of output.

  • line 15

    The pure function gives 10 both times, and would still give 10 on the thousandth call, in any order, from anywhere.

What it prints

10201010

A function that answers correctly and is still not pure

This one passes the same-answer test — 3 kilograms always costs 6 — and fails the other half. Count the lines of output and notice that two calls left four lines behind.

1function shippingCost(weightKg) {2  console.log("working out shipping...");3  return weightKg * 2;4}5 6console.log(shippingCost(3));7console.log(shippingCost(3));
  • line 2

    The side effect. The returned value is fine; it is the extra line on the screen that makes calling this twice different from calling it once.

  • line 6

    Two lines of output come from this single line: the function's own, then the printed result.

  • line 7

    A caller who wanted the number quietly has no way to ask for that. The decision to print was made in the wrong place — inside, rather than by whoever called.

What it prints

working out shipping...6working out shipping...6

Why a function that rolls a die cannot be pinned down

This example is deliberately written so that its output IS fixed, even though the value it produces is not. That is the only way to demonstrate randomness in a place where the expected output has to be stated in advance — which is itself the lesson.

1function rollDie() {2  return Math.floor(Math.random() * 6) + 1;3}4 5const roll = rollDie();6console.log(typeof roll);7console.log(roll >= 1 && roll <= 6);
  • line 2

    Math.random returns a different number nearly every call, so rollDie cannot promise the same answer for the same inputs — it has no inputs at all, and still varies.

  • line 6

    Printing the value itself would make this example unusable, because no fixed expected output could be written for it. All that can be checked is its type — always "number" — and its range.

  • line 7

    This is the shape a test of an impure function is forced into: a property that always holds, rather than an exact answer. It is much weaker, and that weakness is the real cost of impurity.

What it prints

numbertrue

Check yourself

Not scored, not stored. Getting one wrong is the useful part.

One of these functions is pure and one is not. What does the program print?

let visits = 0;function countVisit() {  visits = visits + 1;  return visits;}function describeVisits(count) {  return count + " visits";}console.log(countVisit());console.log(countVisit());console.log(describeVisits(2));console.log(describeVisits(2));

Which of these functions is pure?

Why can a real program not be made entirely of pure functions?

Write it yourself

Exercise · core · 7 tests

Make a function pure

priceWithTax works out a price including tax, and does two things it has no business doing: it appends to receipt, and it prints. Rewrite it so that it only calculates and returns. While you are in there, fix the arithmetic: the answer must be rounded to two decimal places, so 19.99 at a rate of 0.2 gives 23.99 rather than 23.988. Leave the declaration of receipt where it is — the tests use it to check that your function left it alone.

What your code must do

priceWithTax(price, taxRate) returns price plus price times taxRate, rounded to two decimal places, as a number. The same two arguments always give the same number, whenever it is called. It does not assign to receipt or to anything else outside itself, and it prints nothing at all.

Define priceWithTax and receipt at the top level of your code — the tests call them by name.

Runs on your device in a sandbox with no network and no access to this page.

Worth remembering

  • What two questions decide whether a function is pure?

    Does it give the same answer for the same inputs every time, and does anything outside it change when it runs? Pure means yes to the first and no to the second.

  • Is a function that only calls console.log and returns the right value pure?

    No. Printing changes something outside the function, so calling it twice is not the same as calling it once. Return the value and let the caller decide whether to print it.

Check this lesson against the source

We wrote the explanation; we did not invent the facts. This is the page that backs them.

MDN — Math.random()

Check there: The page states that Math.random() returns a floating-point pseudo-random number in the range 0 to less than 1, chosen with approximately uniform distribution — it takes no arguments and cannot be asked for a particular value.

MDN — Math.round()

Check there: That Math.round returns the value rounded to the nearest integer, which is what the two-decimal-place trick in the exercise relies on.

MDN — Functions (JavaScript Guide)

Check there: The guide's treatment of function scope and what a function can reach.

Prefer to read the source in a structured breakdown? Turn this doc into a breakdown

0 of 1 exercise in this lesson solved on this device

Consent version 2026-07-31.1

Cookie preferences