What you'll be able to do

By the end of this page you should be able to tell a pure function from an impure one, spot a side effect, and avoid the beginner trap of changing a shared array or object inside a helper when you meant to return a new value.

Previous post: higher-order functions.

Who this is for

  • People who call a helper twice and get two different answers with the same arguments
  • Beginners who push into an array inside a function and wonder why the original list changed
  • Anyone told to “write pure functions” with no plain example

You can skip this if you already keep calculate-and-return helpers pure and put DOM/network updates in clear impure functions on purpose. Come back when shared mutation surprises you.

Pure: same inputs → same output, no outside change

A pure function’s result depends only on its arguments. It does not read or change hidden outside state, and it does not touch the page, network, or console as part of its job.

function double(n) {
  return n * 2;
}

console.log(double(4));
console.log(double(4));

Both calls return 8. Nothing else changes.

Scenario A — tax math. function tax(amount) { return amount * 0.18; } is easy to trust and test: pass a number, get a number.

Scenario B — string label. function label(title, seconds) { return title + " (" + seconds + "s)"; } — same inputs, same string, every time.

Pure does not mean “only one line.” It means predictable and self-contained.

Impure: side effects (and that is often OK)

A side effect is any change or dependency outside the return value: updating the DOM, logging, writing a global, saving to storage, fetching — or mutating a value the caller still holds.

function showDouble(n) {
  const out = document.getElementById("out");
  out.textContent = String(n * 2);
}

Scenario A — UI helper. You want a side effect: the screen should change. Impure is correct here.

Scenario B — logging while debugging. console.log inside a function is a side effect. Fine while learning; strip or keep on purpose later.

Impure is not “bad.” Unclear impurity is bad — a function named total that also empties a cart is a surprise.

Shared mutation: the impurity that bites beginners

function addItem(list, item) {
  list.push(item);
  return list;
}

const clips = ["Intro"];
addItem(clips, "Outro");
console.log(clips);

Logs ["Intro", "Outro"]. The original clips changed.

Scenario A — you meant to update in place. Then impurity is intentional — name it pushItem or mutateList so the call site knows.

Scenario B — you thought you got a copy. Later React/state code will hurt if helpers mutate by accident. Prefer returning a new array:

function withItem(list, item) {
  return list.concat(item);
}

Same idea for objects: do not assign into a shared object unless that is the documented job.

Same arguments, different results (hidden dependency)

let rate = 0.1;

function tax(amount) {
  return amount * rate;
}

console.log(tax(100));
rate = 0.2;
console.log(tax(100));

Scenario A — config that changes. tax(100) is not pure: it reads outer rate. Two calls, two answers.

Scenario B — fix for purity. Pass the rate in: function tax(amount, rate) { return amount * rate; }. Now the inputs tell the whole story.

When to prefer which

StyleUse when
PureMath, formatting, transforms, anything you want easy to reuse and trust
ImpureDOM updates, events, storage, “talk to the outside world”

Scenario A — split the jobs. Pure double(n), impure show(text). Call show(String(double(n))).

Scenario B — one impure wrapper. Fine for a tiny script. As the file grows, keep calculation pure so bugs have fewer places to hide.

A tiny practice file

Save as pure-impure.html.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Pure vs impure</title>
  </head>
  <body>
    <button id="go">Run</button>
    <pre id="out">Result will show here.</pre>
    <script>
      function double(n) {
        return n * 2;
      }

      function withItem(list, item) {
        return list.concat(item);
      }

      function show(text) {
        document.getElementById("out").textContent = text;
      }

      document.getElementById("go").addEventListener("click", function () {
        const base = ["Intro"];
        const next = withItem(base, "Outro");
        const lines = [];
        lines.push("pure double(5): " + double(5));
        lines.push("base still: " + base.join(", "));
        lines.push("next list: " + next.join(", "));
        show(lines.join("\n"));
      });
    </script>
  </body>
</html>

base stays Intro only; next has both. show is the impure step on purpose.

Mistakes I see a lot

1. Mutating arguments and returning them. Callers keep a surprise reference.

2. Reading globals inside “math” helpers. Results drift when globals change.

3. Calling every side effect “wrong.” DOM updates are impure and necessary.

4. Naming a function getTotal while it also clears the cart. One job, honest name.

5. Copying arrays with list2 = list1. That shares the same array; not a pure “new list.” Use concat, slice, or spread (arrays chapter).

What to try before the next post

  1. Write a pure add(a, b) and call it twice with 2, 3.
  2. Write an impure showSum(a, b) that writes to a paragraph.
  3. Compare push vs concat on a starter array.
  4. Build the tiny HTML file.

Next in this series: errors — try, catch, throw — syntax vs runtime vs logic errors, and never swallowing failures silently.

Try this next outside the series

Functions matter more once they stop being textbook examples and start shaping reusable app code.

  • Node.js async patterns — see functions return promises, accept callbacks, and coordinate real work
  • Diff Checker — compare two function versions before you keep a refactor