Pure Functions

Reviewed & published by Brayan K

A pure function is one that always returns the same output for the same inputs and causes no side effects, meaning it never changes anything outside itself or depends on external state.

Part of the free JavaScript course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

💡 Running Code Locally: While this online editor runs real JavaScript, some advanced examples may have limitations. For the best experience:

Master the principles that drive reliable, predictable, and scalable JavaScript applications

What You'll Learn in This Lesson

Pure Functions — The Foundation of Predictable JavaScript

A pure function is a function that, for the same input, always returns the same output and causes no side effects.

Example: Pure vs Impure

// Pure function - same input always gives same output
const add = (a, b) => a + b;

console.log(add(5, 3)); // 8
console.log(add(5, 3)); // 8 (predictable!)

// Impure function - modifies external state
let count = 0;
function increment() {
  count++; // modifies external state — not pure
  return count;
}

console.log(increment()); // 1
console.log(increment()); // 2 (different output each time!)

Why Pure Functions Matter

Pure functions reduce bugs because they are self-contained. When debugging, you examine inputs and outputs, not the entire program's state.

Worked Example: The Same Job, Done Twice

The add and increment pair above shows the definition. This one shows why you should care. A shopping cart gets a 10% discount, written first the pure way and then the impure way, on the same data — and the output makes the damage visible rather than describing it.

Watch the last line of each pair especially. Calling the pure version twice gives the same answer twice. Calling the impure version twice compounds the discount, because its own first call changed the input it reads the second time.

// WORKED EXAMPLE - the same job done twice: once purely, once impurely.
// This is not about style. The impure version quietly damages data its caller
// still needs, and you can watch it happen in the output.

const cart = [
  { name: "Keyboard", price: 45, qty: 1 },
  { name: "Mouse", price: 20, qty: 2 }
];

// A pure helper: it only reads, so it is safe to call as often as you like.
const totalOf = items => items.reduce((sum, i) => sum + i.price * i.qty, 0);

console.log("starting total: £" + totalOf(cart).toFixed(2));

// ---------- PURE: builds and returns a NEW array ----------
function discountPure(items, percent) {
  return items.map(item => ({
    ...item,                                    // copy every field of the item...
    price: item.price * (1 - percent / 100)     // ...then override just this one
  }));
}

const sale = discountPure(cart, 10);
console.log("pure   -> sale total: £" + totalOf(sale).toFixed(2));
console.log("pure   -> original still: £" + totalOf(cart).toFixed(2));
console.log("pure   -> same array object? " + (sale === cart));

// Call it twice with the same input and you get the same answer. Always.
console.log("pure   -> called again: £" + totalOf(discountPure(cart, 10)).toFixed(2));

// ---------- IMPURE: edits the array it was handed ----------
function discountImpure(items, percent) {
  for (const item of items) {
    item.price = item.price * (1 - percent / 100);   // mutates the CALLER's objects
  }
  return items;
}

const sale2 = discountImpure(cart, 10);
console.log("impure -> sale total: £" + totalOf(sale2).toFixed(2));
console.log("impure -> original now: £" + totalOf(cart).toFixed(2));   // it moved
console.log("impure -> same array object? " + (sale2 === cart));

// And because the input itself changed, calling it again does not repeat:
console.log("impure -> called again: £" + totalOf(discountImpure(cart, 10)).toFixed(2));

// ✅ Expected output:
// starting total: £85.00
// pure   -> sale total: £76.50
// pure   -> original still: £85.00
// pure   -> same array object? false
// pure   -> called again: £76.50
// impure -> sale total: £76.50
// impure -> original now: £76.50
// impure -> same array object? true
// impure -> called again: £68.85
//
// That final £68.85 is the whole bug class in one number. A component
// re-renders, a retry fires, someone double-clicks Apply - and the discount
// compounds. The pure version cannot do that, whatever you call it.

🎯 Your Turn: Make These Two Functions Pure

A to-do list needs two operations: tick one item off, and add a new one. Both are written for you apart from three blanks — and all three are the same idea in different clothes: build something new, never edit what you were given.

// 🎯 YOUR TURN - fill in the three blanks marked ___

const todos = [
  { id: 1, text: "buy milk", done: false },
  { id: 2, text: "write lesson", done: false },
  { id: 3, text: "walk dog", done: false }
];

// 1) Tick one item off WITHOUT touching the original list.
//    .map visits every item and returns a brand-new array.
function completeTodo(list, id) {
  return list.___(todo =>                       // 👉 replace ___ with map
    todo.id === id
      ? { ...todo, done: true }                 // a COPY of that todo, with done flipped
      : todo                                    // every other todo passes through untouched
  );
}

// 2) Add an item WITHOUT using push (push edits the array it is called on).
function addTodo(list, text) {
  return [___, { id: list.length + 1, text: text, done: false }];
  //      👆 replace ___ with  ...list  to spread the old items into a new array
}

const afterComplete = completeTodo(todos, 2);
const afterAdd = addTodo(afterComplete, "make tea");

console.log("original untouched: " + todos.filter(t => t.done).length + " done, " + todos.length + " items");
console.log("new list: " + afterAdd.filter(t => t.done).length + " done, " + afterAdd.length + " items");
console.log("last item: " + afterAdd[afterAdd.length - 1].text);
console.log("same array? " + (afterAdd === ___));   // 👉 compare against todos

// ✅ Expected output once the blanks are filled:
// original untouched: 0 done, 3 items
// new list: 1 done, 4 items
// last item: make tea
// same array? false
//
// If the first line says "1 done", something mutated the original - check that
// blank 1 really is map and not forEach.

Why Pure Functions Improve Performance & Testing

Pure functions enable powerful optimizations that JavaScript engines can automatically apply:

Memoization Example

const memoize = fn => {
  const cache = {};
  return x => cache[x] ?? (cache[x] = fn(x));
};

const slowSquare = x => {
  // Simulate expensive operation
  for (let i = 0; i < 1e8; i++);
  return x * x;
};

const fastSquare = memoize(slowSquare);

console.log(fastSquare(5)); // slow first time
console.log(fastSquare(5)); // instant! (cached)
console.log(fastSquare(10)); // slow first time
console.log(fastSquare(10)); // instant! (cached)

Immutability — Avoiding Accidental State Corruption

Immutability means state never changes directly; new state is returned instead.

Arrays: Mutable vs Immutable

// ❌ Non-immutable code (common bug)
const arr = [1, 2, 3];
arr.push(4); // mutates original
console.log(arr); // [1, 2, 3, 4]

// ✅ Immutable version
const arr2 = [1, 2, 3];
const newArr = [...arr2, 4]; // creates new array
console.log(arr2); // [1, 2, 3] (original unchanged)
console.log(newArr); // [1, 2, 3, 4] (new array)

Objects: Mutable vs Immutable

// ❌ Mutating object directly
const user = { name: "Leo", age: 20 };
user.age = 21; // mutation!
console.log(user); // { name: "Leo", age: 21 }

// ✅ Immutable update
const user2 = { name: "Leo", age: 20 };
const updated = { ...user2, age: 21 }; // new object
console.log(user2); // { name: "Leo", age: 20 } (unchanged)
console.log(updated); // { name: "Leo", age: 21 } (new)

Accidental Mutation — The #1 Cause of Hidden Bugs

Even experienced developers make mistakes by accidentally mutating data:

❌ Common Mistake

function addScore(player, inc) {
  player.score += inc; // mutation!
  return player;
}

const player = { name: "Ava", score: 10 };
const updated = addScore(player, 5);

console.log(player.score); // 15 (original mutated!)
console.log(updated.score); // 15 (same reference)

✅ Better Version

const addScore = (player, inc) => ({
  ...player,
  score: player.score + inc
});

const player = { name: "Ava", score: 10 };
const updated = addScore(player, 5);

console.log(player.score); // 10 (original unchanged!)
console.log(updated.score); // 15 (new object)

Side Effects — Necessary but Dangerous

A side effect is any interaction with the outside world:

Controlled Side Effects

// ❌ Impure - side effect hidden inside
let value = 10;

function update() {
  value = Math.random(); // unpredictable mutation + randomness
}

update();
console.log(value); // ???

// ✅ Better - isolate randomness and state updates
const getRandom = () => Math.random();

const updateValue = (value, random) => value + random;

const newValue = updateValue(10, getRandom());
console.log(newValue); // predictable flow

Pure Core + Isolated Effects = Scalable Architecture

Modern design philosophy splits code into three layers:

Architecture Benefit

This isolates risk and keeps the pure core clean. Testing becomes easier because the pure logic has no dependencies on external state.

Hidden Side Effects That Developers Forget

The following look pure, but they are NOT:

// 1. Using Date() - always different
const getTime = () => Date.now(); // not pure

// 2. Random numbers
const getRandom = () => Math.random(); // not pure

// 3. Accessing outer scoped variables
let rate = 1.2;
const tax = x => x * rate; // depends on external variable

// 4. Mutating parameters
function boost(player) {
  player.power += 1; // mutation!
}

// 5. Logging
function calc(x) {
  console.log(x); // side effect
  return x * 2;
}

// 6. Network requests
async function getUser() {
  return fetch("/user"); // always a side effect
}

Important

Side effects are unavoidable — but detecting them means you can isolate them.

Accidental Mutation with Arrays & Objects

Many built-in methods mutate data. Know which ones!

✅ Non-Mutating Methods

// ❌ Mutation mistake
const scores = [3, 1, 2];
scores.sort((a, b) => b - a); // mutates original!
console.log(scores); // [3, 2, 1] - original changed

// ✅ Correct immutable way
const scores2 = [3, 1, 2];
const sorted = [...scores2].sort((a, b) => b - a);
console.log(scores2); // [3, 1, 2] - original unchanged
console.log(sorted); // [3, 2, 1] - new sorted array

Immutability in Nested Structures

Shallow copies don't protect nested objects:

// ❌ Shallow copy mistake
const state = { user: { name: "Kai", level: 1 } };
const copy = { ...state };

copy.user.level = 2; // also mutates original ❌

console.log(state.user.level); // 2 (original changed!)

// ✅ Correct immutable deep update
const state2 = { user: { name: "Kai", level: 1 } };
const newState = {
  ...state2,
  user: {
    ...state2.user,
    level: 2
  }
};

console.log(state2.user.level); // 1 (original unchanged)
console.log(newState.user.level); // 2 (new state)

Functional Composition with Pure Functions

Pure functions chain beautifully using functional composition:

const double = x => x * 2;
const square = x => x * x;
const addTen = x => x + 10;

const pipe = (...fns) => x => 
  fns.reduce((v, fn) => fn(v), x);

const transform = pipe(
  double,   // 5 -> 10
  square,   // 10 -> 100
  addTen    // 100 -> 110
);

console.log(transform(5)); // 110

// Used everywhere:
// - data processing pipelines
// - validation layers
// - request filtering
// - business logic layers
// - game loops

🎯 Mini-Challenge: A Sales Report Pipeline

Build a report out of three small pure functions and the pipe you just met. No starter logic below, only the brief.

One warning, because it is the trap that catches nearly everyone: of the three array methods you need, two return a new array and one sorts the array in place. The second line of the expected output exists purely to catch that mistake — if your orders come back reordered, you found it.

// 🎯 MINI-CHALLENGE: a sales report, built from pure steps
//
// Write four things, then run it:
//
// 1. biggestFirst(list) -> the same orders sorted by total, highest first.
//                          Careful: .sort() rearranges the array you call it
//                          on. A pure version copies first.
// 2. onlyPaid(list)     -> only the orders whose status is "paid".
// 3. withVat(list)      -> every order with its total multiplied by 1.2 and
//                          rounded with Math.round. Copy each order; do not
//                          edit it.
// 4. report             -> pipe(biggestFirst, onlyPaid, withVat)
//                          where  const pipe = (...fns) => value =>
//                                   fns.reduce((acc, fn) => fn(acc), value);
//
// Then print the two lines at the bottom, which are already written.

const orders = [
  { id: "a1", total: 120, status: "paid" },
  { id: "a2", total: 45, status: "pending" },
  { id: "a3", total: 260, status: "paid" },
  { id: "a4", total: 80, status: "cancelled" },
  { id: "a5", total: 310, status: "paid" }
];

// your code here

console.log(report(orders).map(o => o.id + ":" + o.total).join(", "));
console.log("orders untouched: " + orders.map(o => o.id).join(", "));

// ✅ Expected output:
// a5:372, a3:312, a1:144
// orders untouched: a1, a2, a3, a4, a5
//
// Get the second line wrong and it reads "a5, a3, a1, a4, a2" instead - your
// sort reordered the caller's data, which is exactly the impurity this whole
// lesson is about.

Real-World Architecture Pattern

Professional engineers separate pure logic from side effects at architectural boundaries:

// Pure logic
const applyDiscount = (price, discount) => 
  price - price * discount;

// Impure boundary (side effects isolated here)
async function checkout(cart) {
  const response = await fetch("/api/pay", {
    method: "POST",
    body: JSON.stringify(cart)
  });

  const data = await response.json();
  return applyDiscount(data.amount, 0.1);
}

// This separation makes:
// - testing easier
// - debugging simpler
// - scaling straightforward
// - rewriting painless

Key Takeaway

Pure functions, immutability, and controlled side effects aren't just "good practices" — they form the backbone of reliable, scalable, and bug-resistant JavaScript systems. Master these concepts to gain full control over application complexity, performance, and long-term maintainability.

Where These Patterns Appear in Real Life

Frontend Frameworks

Backend Systems

Data Processing

Modern Tools

Lesson Complete!

You've mastered pure functions, immutability, and controlled side effects — the backbone of reliable, scalable JavaScript. You now know how to write code that is predictable, testable, and bug-resistant.

📋 Quick Reference

ConceptKey Rule
Pure functionSame input → same output, no side effects
ImmutabilityReturn new objects/arrays, never mutate
Side effectAny interaction with outside world (DOM, API, logging)
MemoizationCache pure function results for performance
Compositionpipe(...fns)(x) — chain pure functions

Practice quiz

What are the two rules of a pure function?

  • Use const and return a value
  • Be async and cache results
  • Same input gives same output, and no external state modification
  • Log to console and mutate inputs

Answer: Same input gives same output, and no external state modification. Pure functions return the same output for the same input and never modify external state.

What does immutability mean in this lesson?

  • State never changes directly; new state is returned instead
  • Variables are constant
  • Objects are frozen forever
  • Functions cannot be called twice

Answer: State never changes directly; new state is returned instead. Immutability means returning new objects/arrays rather than mutating existing state.

Which array method mutates the original array?

  • map()
  • filter()
  • slice()
  • push()

Answer: push(). push() mutates the original; map, filter, and slice return new arrays.

Why does copy.user.level = 2 also change the original when copy = { ...state }?

  • Spread is broken
  • A shallow copy still shares nested object references
  • level is a getter
  • Objects cannot be copied

Answer: A shallow copy still shares nested object references. Spread makes a shallow copy, so nested objects are shared by reference.

Which of these is a side effect?

  • Fetching data or logging to the console
  • Returning a + b
  • Multiplying two numbers
  • Declaring a const

Answer: Fetching data or logging to the console. Any interaction with the outside world (fetch, DOM, logging, randomness) is a side effect.

What is the output of the pipe transform: double, then square, then addTen applied to 5?

  • 100
  • 60
  • 110
  • 35

Answer: 110. 5*2=10, 10*10=100, 100+10=110.

Why is const tax = x => x * rate; (where rate is an outer variable) NOT pure?

  • It uses an arrow function
  • It depends on an external variable
  • It returns a number
  • It has too many arguments

Answer: It depends on an external variable. Depending on an external variable means the result can change without the input changing.

What does memoization do for a pure function?

  • Mutates its arguments
  • Runs it on another thread
  • Makes it impure
  • Caches results so repeated calls with the same input are fast

Answer: Caches results so repeated calls with the same input are fast. Memoization caches pure-function results, returning instantly on repeat inputs.

In the architecture pattern, which layer contains API calls and DOM updates?

  • Pure logic layer
  • Side-effect layer
  • Coordinator layer
  • Memoization layer

Answer: Side-effect layer. The side-effect layer handles API calls, storage, and DOM updates, kept separate from pure logic.

After const updated = addScore(player, 5) using the immutable version (spread), what is player.score (originally 10)?

  • 15
  • 5
  • 10
  • undefined

Answer: 10. The immutable version returns a new object, so the original player.score stays 10.

Continue this course