cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

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.

By the end you can
  • 01
    Define purityExplain same input, same output, with no observable side effects.
  • 02
    Spot impurityClassify time, randomness, globals, logging, network calls, and mutation.
  • 03
    Refactor 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.

Real-life analogyA pure function is a vending machine

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.

The working definition

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

INTERACTIVE

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

Purity detector: run each function twice
Gallery of tiny functionsPop out in the code editor (opens in a new tab)JavaScript
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;}
Two calls, same inputPure
First call11
Second call11
Outside worldnothing changed
Try it yourself

Pure: Same input, same return value, and no observable outside change.

The detector uses controlled time and randomness so the lesson can show the pattern safely. Real 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?”

Pure or impure?
  • 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)}`;
Try it yourself
0 of 8 correct

Sort each card by the behavior it describes. Read the explanation even when you get it right.

Choose a category for every card. You can change an answer at any time; Reset clears them all.
Hydration note

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 THROUGH

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

Real-life analogyA recipe card you can replace with the dish

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.

Replace the call with its value
Step 0 of 4Ready
Your turn: follow the blue line

A pure call can be replaced with its returned value. Step through the two versions.

Running in
  1. script
Next: line 5
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
function add(a, b) {  return a + b;} const replaced = 5 * 5;console.log(total);console.log(replaced);
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
Switch the expression

Changing the expression starts a fresh replay.

A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

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 THROUGH

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

Real-life analogyA restaurant kitchen with a front desk

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.

Refactor checkout into a pure core
Step 0 of 5Ready
Your turn: follow the blue line

Step through the original checkout. Notice each place it reaches outside itself.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
  { 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());
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
Choose which version to replay

The source changes with the setting; every value shown is from the recorded run.

A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

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.

A professional habit

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

INTERACTIVE

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

Testability lab: fixed inputs beat flaky tests
Deterministic pure-core testPop out in the code editor (opens in a new tab)JavaScript
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);
Simulated test reportstable
Pure totals15 and 15
Pure testpass
Impure-style totals15 and 20
Flaky testfail this time
Try it yourself

The pure-core test passes every run because time and randomness are fixed inputs.

The flaky test is simulated with controlled random values so you can see why real randomness makes tests intermittent.

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

INTERACTIVE

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

Mutation lab: push or copy?
Mutating vs returning a copyPop out in the code editor (opens in a new tab)JavaScript
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);
Identity checksarrays
Caller list[learn, practice]
list === changedtrue
list === copiedfalse
Copied value[learn, practice, quiz]
Try it yourself

push returns the same array after changing it. The spread version returns a new array, so the caller's original list stays separate.

This connects to Objects & references: arrays are objects, and a function can mutate the same object the caller holds.

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.

Pure core vs imperative shell
IdeaPure functionImpure function
InputsAll important information is passed as argumentsReads globals, time, random state, DOM, storage, or network
OutputsReturns the answerMay return an answer and also change something elsewhere
Same call twiceSame observable resultMay differ or alter later calls
TestingUse fixed inputs and assert the return valueMock, 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.

Memoization preview

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 EXERCISES
Exercise 1 · Warm-upPredict a pure total

Work out the output. Is the function pure?

Starter codePop out in the code editor (opens in a new tab)JavaScript
function total(items) {
  return items.reduce((sum, item) => sum + item.price, 0);
}
console.log(total([{ price: 2 }, { price: 3 }]));

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticeFind the hidden state

    Predict both printed lines, then name the side effect.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    let calls = 0;
    function countCalls() {
      calls += 1;
      return calls;
    }
    console.log(countCalls());
    console.log(countCalls());

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeRefactor a greeting

      Rewrite a time-based greeting so the calculation accepts hour as an input. Check the output for Ada at 9.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      function greeting(name, hour) {
        return hour < 12 ? "Good morning, " + name : "Hello, " + name;
      }
      console.log(greeting("Ada", 9));

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeSort without surprising the caller

        Use a copy before sorting. What does the original array print?

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        function alphabetic(items) {
          return [...items].sort();
        }
        const original = ["pear", "apple"];
        const sorted = alphabetic(original);
        console.log(original.join(","));
        console.log(sorted.join(","));

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeDraw a shell around a feature

          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.

            Quiz: check your understanding

            7 QUESTIONS

            Choose an answer, then read the explanations. The wrong answers are designed to teach.

            Lesson quiz · 7 questionsScore: first tries count
            1. Question 1 of 7Which description best defines a pure function?

              Choose an answer to see the explanation.

            2. Question 2 of 7What does this counter example print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              let count = 0;
              function next() {
                count += 1;
                return count;
              }
              console.log(next());
              console.log(next());

              Choose an answer to see the explanation.

            3. Question 3 of 7Which line is a side effect?

              Choose an answer to see the explanation.

            4. Question 4 of 7What does replacing a pure call with its value mean?

              Choose an answer to see the explanation.

            5. Question 5 of 7What does this copy example print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              function 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.

            6. Question 6 of 7Why inject now and rng into a checkout calculation?

              Choose an answer to see the explanation.

            7. Question 7 of 7When is console.log pure?

              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.

            CompleteFrontend Clear concepts. Working examples.