Learn pure functions in JavaScript. Understand the two rules of purity, avoid side effects, and write testable, predictable code with immutable patterns.
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.
Copy
Ask AI
// A pure function: same input always gives same outputfunction add(a, b) { return a + b}add(2, 3) // 5add(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.
A pure function is a function that follows two simple rules:
Same input → Same output: Given the same arguments, it always returns the same result
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.
Copy
Ask AI
// ✓ PURE: Follows both rulesfunction double(x) { return x * 2}double(5) // 10double(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:
Copy
Ask AI
// ❌ 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!
Copy
Ask AI
// ❌ IMPURE: Breaks rule 2 (has a side effect)let total = 0function addToTotal(x) { total += x // Modifies external variable! return total}addToTotal(5) // 5addToTotal(5) // 10. Different result because total changed!
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.
Copy
Ask AI
┌─────────────────────────────────────────────────────────────────────────┐│ 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.
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:
Copy
Ask AI
// ✓ PURE: Math.max always returns the same result for the same inputsMath.max(2, 8, 5) // 8Math.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.
Using new Date() makes functions impure because the output depends on when you call them:
Copy
Ask AI
// ❌ IMPURE: Output depends on when you call itfunction getGreeting(name) { const hour = new Date().getHours() if (hour < 12) return `Good morning, ${name}` return `Good afternoon, ${name}`}// Same input, different output depending on time of day
// ✓ PURE: Tax rate is now an input, not external statefunction calculateTotal(price, taxRate) { return price + (price * taxRate)}calculateTotal(100, 0.08) // 108calculateTotal(100, 0.08) // 108, always the samecalculateTotal(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.
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.
// ❌ IMPURE: Multiple side effectsfunction 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 effectsfunction 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 most common way developers accidentally create impure functions is by mutating objects or arrays that were passed in.
Copy
Ask AI
// ❌ IMPURE: Mutates the input arrayfunction 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.
Copy
Ask AI
// ✓ PURE: Returns a new array, original unchangedfunction 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
// ✓ SAFE: Deep clone for nested objectsconst 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.
// ❌ IMPURE: sort() mutates the original array!function getSorted(numbers) { return numbers.sort((a, b) => a - b)}// ✓ PURE: Copy first, then sortfunction getSorted(numbers) { return [...numbers].sort((a, b) => a - b)}// ✓ PURE (ES2023+): Use toSorted() which returns a new arrayfunction 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.
Writing pure functions isn’t just about following rules. It brings real, practical benefits:
1. Easier to Test
Pure functions are a testing dream. No mocking, no setup, no cleanup. Just call the function and check the result.
Copy
Ask AI
// Testing a pure function is trivialfunction add(a, b) { return a + b}// Testexpect(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.
2. Easier to Debug
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.
Copy
Ask AI
// 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}
3. Safe to Cache (Memoization)
Since pure functions always return the same output for the same input, you can safely cache their results. This is called memoization.
Copy
Ask AI
// Expensive calculation - safe to cache because it's purefunction 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.
4. Safe to Parallelize
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.
Copy
Ask AI
// 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))])
5. Easier to Understand
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.
Copy
Ask AI
// You can understand this function completely by reading itfunction 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.
// IMPURE: Reads from DOMfunction 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 DOMfunction displayError(message) { document.querySelector('#error').textContent = message}// Orchestration: impure code at the edgesfunction 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.
Question 1: What two rules define a pure function?
Answer:A pure function must follow both rules:
Same input → Same output: Given the same arguments, it always returns the same result (referential transparency)
No side effects: It doesn’t modify anything outside itself (no mutations, no I/O, no external state changes)
Copy
Ask AI
// Pure: follows both rulesfunction multiply(a, b) { return a * b}
Question 2: Is this function pure? Why or why not?
Copy
Ask AI
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:
Copy
Ask AI
function greet(name, time) { return `Hello, ${name}! The time is ${time}`}
Question 3: What's wrong with this function?
Copy
Ask AI
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:
Copy
Ask AI
function addToCart(cart, item) { return [...cart, item]}
Question 4: How do you safely update a nested object in a pure function?
Answer:Use structuredClone() for a deep copy, or carefully spread at each level:
Note: A simple { ...user } shallow copy would still share the nested address object with the original.
Question 5: Why are pure functions easier to test?
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
Copy
Ask AI
// Testing a pure function - simple and straightforwardexpect(add(2, 3)).toBe(5)expect(formatName(' ALICE ')).toBe('alice')expect(isValidEmail('[email protected]')).toBe(true)
Question 6: When is it okay to have impure functions?
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.
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.
What are side effects in JavaScript?
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.
Why are pure functions easier to test?
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.
Is console.log a side effect?
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.
What is referential transparency?
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.