Skip to main content
Why does the same function sometimes give you different results? Why is some code easy to test while other code requires elaborate setup and mocking? Why do bugs seem to appear “randomly” when your logic looks correct? The answer often comes down to pure functions. They’re at the heart of functional programming, and understanding them will change how you write JavaScript.
// A pure function: same input always gives same output
function add(a, b) {
  return a + b
}

add(2, 3)  // 5
add(2, 3)  // 5, always 5, no matter when or where you call it
A pure function is simple, predictable, and trustworthy. Once you understand why, you’ll start seeing opportunities to write cleaner code everywhere.
What you’ll learn in this guide:
  • The two rules that make a function “pure”
  • What side effects are and how they create bugs
  • How to identify pure vs impure functions
  • Practical patterns for avoiding mutations
  • When pure functions aren’t possible (and what to do instead)
  • Why purity makes testing and debugging much easier
Helpful background: This guide references object and array mutations frequently. If you’re not comfortable with how JavaScript handles primitives vs objects, read that guide first. It explains why const arr = [1,2,3]; arr.push(4) works but shouldn’t surprise you.

What is a Pure Function?

A pure function is a function that follows two simple rules:
  1. Same input → Same output: Given the same arguments, it always returns the same result
  2. No side effects: It doesn’t change anything outside itself
That’s it. If a function follows both rules, it’s pure. If it breaks either rule, it’s impure. This concept comes directly from mathematics, where functions are defined as deterministic mappings from inputs to outputs. According to the State of JS 2023 survey, functional programming concepts like pure functions and immutability continue to grow in adoption across the JavaScript ecosystem.
// ✓ PURE: Follows both rules
function double(x) {
  return x * 2
}

double(5)  // 10
double(5)  // 10, always 10
Using Math.random() breaks purity because it introduces randomness. As MDN explains, Math.random() returns a pseudo-random number, meaning it depends on internal engine state rather than your function’s arguments:
// ❌ IMPURE: Breaks rule 1 (different output for same input)
function randomDouble(x) {
  return x * Math.random()
}

randomDouble(5)  // 2.3456...
randomDouble(5)  // 4.1234... different every time!
// ❌ IMPURE: Breaks rule 2 (has a side effect)
let total = 0

function addToTotal(x) {
  total += x  // Modifies external variable!
  return total
}

addToTotal(5)  // 5
addToTotal(5)  // 10. Different result because total changed!

The Kitchen Recipe Analogy

Think of a pure function like a recipe. If you give a recipe the same ingredients, you get the same dish every time. The recipe doesn’t care what time it is, what else is in your kitchen, or what you cooked yesterday.
┌─────────────────────────────────────────────────────────────────────────┐
│                        PURE VS IMPURE FUNCTIONS                          │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│  PURE FUNCTION (Like a Recipe)                                           │
│  ─────────────────────────────                                           │
│                                                                          │
│     Ingredients           Recipe            Dish                         │
│    ┌───────────┐       ┌─────────┐       ┌───────┐                       │
│    │ 2 eggs    │       │         │       │       │                       │
│    │ flour     │ ────► │  mix &  │ ────► │ cake  │                       │
│    │ sugar     │       │  bake   │       │       │                       │
│    └───────────┘       └─────────┘       └───────┘                       │
│                                                                          │
│    ✓ Same ingredients = Same cake, every time                            │
│    ✓ Doesn't rearrange your kitchen                                      │
│    ✓ Doesn't depend on the weather                                       │
│                                                                          │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│  IMPURE FUNCTION (Unpredictable Chef)                                    │
│  ────────────────────────────────────                                    │
│                                                                          │
│    ┌───────────┐       ┌─────────┐       ┌───────┐                       │
│    │ 2 eggs    │       │ checks  │       │  ???  │                       │
│    │ flour     │ ────► │ clock,  │ ────► │       │                       │
│    │ sugar     │       │ mood... │       │       │                       │
│    └───────────┘       └─────────┘       └───────┘                       │
│                                                                          │
│    ✗ Same ingredients might give different results                       │
│    ✗ Might rearrange your whole kitchen while cooking                    │
│    ✗ Depends on external factors you can't control                       │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘
A pure function is like a recipe: predictable, self-contained, and trustworthy. An impure function is like a chef who checks the weather, changes the recipe based on mood, and rearranges your kitchen while cooking.

Rule 1: Same Input → Same Output

This rule is also called referential transparency. It means you could replace a function call with its result and the program would work exactly the same. This property is fundamental to functional programming and is what enables tools like React’s useMemo to safely cache function results — as the React documentation notes, memoization relies on functions being pure. Math.max() is a great example of a pure function:
// ✓ PURE: Math.max always returns the same result for the same inputs
Math.max(2, 8, 5)  // 8
Math.max(2, 8, 5)  // 8, always 8

// You could replace Math.max(2, 8, 5) with 8 anywhere in your code
// and nothing would change. That's referential transparency.

What Breaks This Rule?

Anything that makes the output depend on something other than the inputs:
// ❌ IMPURE: Output depends on randomness
function getRandomDiscount(price) {
  return price * Math.random()
}

getRandomDiscount(100)  // 47.23...
getRandomDiscount(100)  // 82.91... different!

How to Fix It

Pass everything the function needs as arguments:
// ✓ PURE: Tax rate is now an input, not external state
function calculateTotal(price, taxRate) {
  return price + (price * taxRate)
}

calculateTotal(100, 0.08)  // 108
calculateTotal(100, 0.08)  // 108, always the same
calculateTotal(100, 0.10)  // 110 — different input, different output (that's fine!)
Quick test for Rule 1: Can you predict the output just by looking at the inputs? If you need to know the current time, check a global variable, or run it to find out, it’s probably not pure.

Rule 2: No Side Effects

A side effect is anything a function does besides computing and returning a value. Side effects are actions that affect the world outside the function.

Common Side Effects

Side EffectExampleWhy It’s a Problem
Mutating inputarray.push(item)Changes data the caller might still be using
Modifying external variablescounter++Creates hidden dependencies
Console outputconsole.log()Does something besides returning a value
DOM manipulationelement.innerHTML = '...'Changes the page state
HTTP requestsfetch('/api/data')Communicates with external systems
Writing to storagelocalStorage.setItem()Persists data outside the function
Throwing exceptionsthrow new Error()Breaks normal control flow (debated)
// ❌ IMPURE: Multiple side effects
function processUser(user) {
  user.lastLogin = new Date()        // Side effect: mutates input
  console.log(`User ${user.name}`)   // Side effect: console output
  userCount++                         // Side effect: modifies external variable
  return user
}

// ✓ PURE: Returns new data, no side effects
function processUser(user, loginTime) {
  return {
    ...user,
    lastLogin: loginTime
  }
}
Is console.log() really that bad? Technically, yes. It’s a side effect. But practically? It’s fine for debugging. The key is understanding that it makes your function impure. Don’t let console.log statements slip into production code that should be pure.

The #1 Pure Functions Mistake: Mutations

The most common way developers accidentally create impure functions is by mutating objects or arrays that were passed in.
// ❌ IMPURE: Mutates the input array
function addItem(cart, item) {
  cart.push(item)  // This changes the original cart!
  return cart
}

const myCart = ['apple', 'banana']
const newCart = addItem(myCart, 'orange')

console.log(myCart)   // ['apple', 'banana', 'orange'] — Original changed!
console.log(newCart)  // ['apple', 'banana', 'orange']
console.log(myCart === newCart)  // true — They're the same array!
This creates bugs because any other code using myCart now sees unexpected changes. The fix is simple: return a new array instead of modifying the original.
// ✓ PURE: Returns a new array, original unchanged
function addItem(cart, item) {
  return [...cart, item]  // Spread into new array
}

const myCart = ['apple', 'banana']
const newCart = addItem(myCart, 'orange')

console.log(myCart)   // ['apple', 'banana'] — Original unchanged!
console.log(newCart)  // ['apple', 'banana', 'orange']
console.log(myCart === newCart)  // false — Different arrays

Shallow Copy Trap

Watch out: the spread operator only creates a shallow copy. Nested objects are still shared:
// ⚠️ DANGER: Shallow copy with nested objects
const user = {
  name: 'Alice',
  address: { city: 'NYC', zip: '10001' }
}

const updatedUser = { ...user, name: 'Bob' }

// Top level is a new object...
console.log(user === updatedUser)  // false ✓

// But nested object is SHARED
updatedUser.address.city = 'LA'
console.log(user.address.city)  // 'LA'. Original changed!
For nested objects, use structuredClone() for a deep copy:
// ✓ SAFE: Deep clone for nested objects
const user = {
  name: 'Alice',
  address: { city: 'NYC', zip: '10001' }
}

const updatedUser = {
  ...structuredClone(user),
  name: 'Bob'
}

updatedUser.address.city = 'LA'
console.log(user.address.city)  // 'NYC' — Original safe!
Limitation: structuredClone() cannot clone functions or DOM nodes. It will throw a DataCloneError for these types.
The Trap: Spread operator (...) only copies one level deep. If you have nested objects or arrays, mutations to the nested data will affect the original. Use structuredClone() for deep copies, or see our Primitives vs Objects guide for more patterns.

Immutable Patterns for Pure Functions

Here are the most common patterns for writing pure functions that handle objects and arrays:

Updating Objects

// ❌ IMPURE: Mutates the object
function updateEmail(user, email) {
  user.email = email
  return user
}

// ✓ PURE: Returns new object with updated property
function updateEmail(user, email) {
  return { ...user, email }
}

Adding to Arrays

// ❌ IMPURE: Mutates the array
function addTodo(todos, newTodo) {
  todos.push(newTodo)
  return todos
}

// ✓ PURE: Returns new array with item added
function addTodo(todos, newTodo) {
  return [...todos, newTodo]
}

Removing from Arrays

// ❌ IMPURE: Mutates the array
function removeTodo(todos, index) {
  todos.splice(index, 1)
  return todos
}

// ✓ PURE: Returns new array without the item
function removeTodo(todos, index) {
  return todos.filter((_, i) => i !== index)
}

Updating Array Items

// ❌ IMPURE: Mutates item in array
function completeTodo(todos, index) {
  todos[index].completed = true
  return todos
}

// ✓ PURE: Returns new array with updated item
function completeTodo(todos, index) {
  return todos.map((todo, i) => 
    i === index ? { ...todo, completed: true } : todo
  )
}

Sorting Arrays

// ❌ IMPURE: sort() mutates the original array!
function getSorted(numbers) {
  return numbers.sort((a, b) => a - b)
}

// ✓ PURE: Copy first, then sort
function getSorted(numbers) {
  return [...numbers].sort((a, b) => a - b)
}

// ✓ PURE (ES2023+): Use toSorted() which returns a new array
function getSorted(numbers) {
  return numbers.toSorted((a, b) => a - b)
}
ES2023 added non-mutating versions of several array methods: toSorted(), toReversed(), toSpliced(), and with(). These are perfect for pure functions. Check browser support before using in production.

Why Pure Functions Matter

Writing pure functions isn’t just about following rules. It brings real, practical benefits:
Pure functions are a testing dream. No mocking, no setup, no cleanup. Just call the function and check the result.
// Testing a pure function is trivial
function add(a, b) {
  return a + b
}

// Test
expect(add(2, 3)).toBe(5)
expect(add(-1, 1)).toBe(0)
expect(add(0, 0)).toBe(0)
// Done! No mocks, no setup, no teardown
Compare this to testing a function that reads from the DOM, makes API calls, or depends on global state. You’d need elaborate setup just to run one test.
When something goes wrong, pure functions narrow down the problem fast. If a pure function returns the wrong value, the bug is in that function. It can’t be caused by some other code changing global state.
// If calculateTax(100, 0.08) returns the wrong value,
// the bug MUST be inside calculateTax.
// No need to check what other code ran before it.
function calculateTax(amount, rate) {
  return amount * rate
}
Since pure functions always return the same output for the same input, you can safely cache their results. This is called memoization.
// Expensive calculation - safe to cache because it's pure
function fibonacci(n) {
  if (n <= 1) return n
  return fibonacci(n - 1) + fibonacci(n - 2)
}

// With memoization, fibonacci(40) computes once, then returns cached result
You can’t safely cache impure functions because they might need to return different values even with the same inputs.
Pure functions don’t depend on shared state, so they can safely run in parallel. This is how libraries like TensorFlow process massive datasets across multiple CPU cores or GPU threads.
// These can all run at the same time - no conflicts!
const results = await Promise.all([
  processChunk(data.slice(0, 1000)),
  processChunk(data.slice(1000, 2000)),
  processChunk(data.slice(2000, 3000))
])
When you see a pure function, you know everything it can do is right there in the code. No hidden dependencies, no spooky action at a distance.
// You can understand this function completely by reading it
function formatPrice(cents, currency = 'USD') {
  const dollars = cents / 100
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency
  }).format(dollars)
}
This function uses Intl.NumberFormat but remains pure because the same inputs always produce the same formatted output.

When Pure Functions Aren’t Possible

Let’s be realistic: you can’t build useful applications with only pure functions. At some point you need to:
  • Read from and write to the DOM
  • Make HTTP requests
  • Log errors
  • Save to localStorage
  • Respond to user events
The strategy is to push impure code to the edges of your application. Keep the core logic pure, and isolate the impure parts.
┌─────────────────────────────────────────────────────────────────────────┐
│                      STRUCTURE OF A WELL-DESIGNED APP                    │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│  EDGES (Impure)              CORE (Pure)              EDGES (Impure)     │
│  ──────────────              ──────────              ──────────────      │
│                                                                          │
│  ┌──────────────┐         ┌──────────────┐         ┌──────────────┐      │
│  │ Read from    │         │ Transform    │         │ Write to     │      │
│  │ DOM, API,    │ ──────► │ Calculate    │ ──────► │ DOM, API,    │      │
│  │ user input   │         │ Process      │         │ console      │      │
│  └──────────────┘         └──────────────┘         └──────────────┘      │
│                                                                          │
│     INPUT                    LOGIC                    OUTPUT             │
│   (impure)                  (pure)                  (impure)             │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

Example: Separating Pure from Impure

// IMPURE: Reads from DOM
function getUserInput() {
  return document.querySelector('#username').value
}

// PURE: Transforms data (no DOM access)
function formatUsername(name) {
  return name.trim().toLowerCase()
}

// PURE: Validates data (no side effects)
function isValidUsername(name) {
  return name.length >= 3 && name.length <= 20
}

// IMPURE: Writes to DOM
function displayError(message) {
  document.querySelector('#error').textContent = message
}

// Orchestration: impure code at the edges
function handleSubmit() {
  const raw = getUserInput()           // Impure: read
  const formatted = formatUsername(raw) // Pure: transform
  const isValid = isValidUsername(formatted) // Pure: validate
  
  if (!isValid) {
    displayError('Username must be 3-20 characters') // Impure: write
  }
}
The pure functions (formatUsername, isValidUsername) are easy to test and reuse. The impure functions are isolated at the edges where they’re easy to find and manage.

Key Takeaways

The key things to remember about pure functions:
  1. Two rules define purity: same input → same output, and no side effects
  2. Side effects include mutations, console.log, DOM access, HTTP requests, randomness, and current time
  3. Mutations are the #1 trap. Use spread operator or structuredClone() to return new data instead
  4. Shallow copies aren’t enough for nested objects. The spread operator only copies one level deep
  5. Pure functions are easier to test. No mocking, no setup. Just input and expected output
  6. Pure functions are easier to debug. If the output is wrong, the bug is in that function
  7. Pure functions can be cached. Same input always means same output, so memoization is safe
  8. You can’t avoid impurity entirely. The goal is to isolate it at the edges of your application
  9. console.log is technically impure but acceptable for debugging. Just don’t let it slip into logic that should be pure
  10. ES2023 added toSorted(), toReversed() and other non-mutating array methods. Use them when you can!

Test Your Knowledge

Answer:A pure function must follow both rules:
  1. Same input → Same output: Given the same arguments, it always returns the same result (referential transparency)
  2. No side effects: It doesn’t modify anything outside itself (no mutations, no I/O, no external state changes)
// Pure: follows both rules
function multiply(a, b) {
  return a * b
}
function greet(name) {
  return `Hello, ${name}! The time is ${new Date().toLocaleTimeString()}`
}
Answer:No, this function is impure. It breaks Rule 1 (same input → same output) because it uses new Date(). Calling greet('Alice') at 10:00 AM gives a different result than calling it at 3:00 PM, even though the input is the same.To make it pure, pass the time as a parameter:
function greet(name, time) {
  return `Hello, ${name}! The time is ${time}`
}
function addToCart(cart, item) {
  cart.push(item)
  return cart
}
Answer:This function mutates its input. The push() method modifies the original cart array, which is a side effect. Any other code using that cart array will see unexpected changes.Fix it by returning a new array:
function addToCart(cart, item) {
  return [...cart, item]
}
Answer:Use structuredClone() for a deep copy, or carefully spread at each level:
// Option 1: structuredClone (simplest)
function updateCity(user, newCity) {
  const copy = structuredClone(user)
  copy.address.city = newCity
  return copy
}

// Option 2: Spread at each level
function updateCity(user, newCity) {
  return {
    ...user,
    address: {
      ...user.address,
      city: newCity
    }
  }
}
Note: A simple { ...user } shallow copy would still share the nested address object with the original.
Answer:Pure functions only depend on their inputs and only produce their return value. This means:
  • No setup needed: You don’t need to configure global state, mock APIs, or set up DOM elements
  • No cleanup needed: The function doesn’t change anything, so there’s nothing to reset
  • Predictable: Same input always gives same output, so tests are deterministic
  • Isolated: If a test fails, the bug must be in that function
// Testing a pure function - simple and straightforward
expect(add(2, 3)).toBe(5)
expect(formatName('  ALICE  ')).toBe('alice')
expect(isValidEmail('[email protected]')).toBe(true)
Answer:Impure functions are necessary for any real application. You need them to:
  • Read user input from the DOM
  • Make HTTP requests to APIs
  • Write output to the screen
  • Save data to localStorage or databases
  • Log errors and debugging info
The strategy is to isolate impurity at the edges of your application. Keep your core business logic in pure functions, and use impure functions only for I/O operations. This gives you the best of both worlds: testable, predictable logic with the ability to interact with the outside world.

Frequently Asked Questions

A pure function always returns the same output for the same input and produces no side effects — it doesn’t modify external variables, the DOM, or any state outside itself. As MDN’s glossary explains, JavaScript functions are objects that can encapsulate logic, and pure functions use that encapsulation to guarantee predictability.
Side effects are any observable changes a function makes beyond returning a value. Common side effects include modifying global variables, writing to the DOM, making HTTP requests, logging to the console, and writing to local storage. Pure functions avoid all of these.
Pure functions need no mocking, no setup, and no teardown. You pass inputs and assert outputs — that’s it. According to the Stack Overflow 2023 Developer Survey, testing difficulty is one of the top challenges developers face, and pure functions directly reduce that complexity.
Yes. console.log() writes to an external output stream (the browser console or terminal), which is an observable effect beyond returning a value. A function that calls console.log() is technically impure, even though it’s harmless in practice. In production code, logging is an acceptable impurity kept at the edges of your application.
Referential transparency means you can replace a function call with its return value without changing the program’s behavior. For example, if add(2, 3) always returns 5, you can substitute 5 anywhere add(2, 3) appears. This property makes code easier to reason about and enables compiler optimizations.


Reference

Articles

Videos

Last modified on February 17, 2026