docs/concepts/pure-functions.mdx
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.
<Info> **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 </Info> <Warning> **Helpful background:** This guide references object and array mutations frequently. If you're not comfortable with how JavaScript handles [primitives vs objects](/concepts/primitives-objects), read that guide first. It explains why `const arr = [1,2,3]; arr.push(4)` works but shouldn't surprise you. </Warning>A pure function is a function that follows two simple rules:
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!
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.
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.
Anything that makes the output depend on something other than the inputs:
<Tabs> <Tab title="Random Values"> ```javascript // ❌ IMPURE: Output depends on randomness function getRandomDiscount(price) { return price * Math.random() }getRandomDiscount(100) // 47.23...
getRandomDiscount(100) // 82.91... different!
```
</Tab>
<Tab title="Current Time">
Using new Date() makes functions impure because the output depends on when you call them:
```javascript
// ❌ IMPURE: Output depends on when you call it
function 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
```
function calculateTotal(price) {
return price + (price * taxRate)
}
calculateTotal(100) // 108
taxRate = 0.10
calculateTotal(100) // 110. Different!
```
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!)
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.
| Side Effect | Example | Why It's a Problem |
|---|---|---|
| Mutating input | array.push(item) | Changes data the caller might still be using |
| Modifying external variables | counter++ | Creates hidden dependencies |
| Console output | console.log() | Does something besides returning a value |
| DOM manipulation | element.innerHTML = '...' | Changes the page state |
| HTTP requests | fetch('/api/data') | Communicates with external systems |
| Writing to storage | localStorage.setItem() | Persists data outside the function |
| Throwing exceptions | throw 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
}
}
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
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!
Here are the most common patterns for writing pure functions that handle objects and arrays:
// ❌ 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 }
}
// ❌ 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]
}
// ❌ 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)
}
// ❌ 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
)
}
// ❌ 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)
}
Writing pure functions isn't just about following rules. It brings real, practical benefits:
<AccordionGroup> <Accordion title="1. Easier to Test"> Pure functions are a testing dream. No mocking, no setup, no cleanup. Just call the function and check the result.```javascript
// 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.
```javascript
// 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
}
```
```javascript
// 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.
```javascript
// 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))
])
```
```javascript
// 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`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat) but remains pure because the same inputs always produce the same formatted output.
Let's be realistic: you can't build useful applications with only pure functions. At some point you need to:
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) │
│ │
└─────────────────────────────────────────────────────────────────────────┘
// 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.
Two rules define purity: same input → same output, and no side effects
Side effects include mutations, console.log, DOM access, HTTP requests, randomness, and current time
Mutations are the #1 trap. Use spread operator or structuredClone() to return new data instead
Shallow copies aren't enough for nested objects. The spread operator only copies one level deep
Pure functions are easier to test. No mocking, no setup. Just input and expected output
Pure functions are easier to debug. If the output is wrong, the bug is in that function
Pure functions can be cached. Same input always means same output, so memoization is safe
You can't avoid impurity entirely. The goal is to isolate it at the edges of your application
console.log is technically impure but acceptable for debugging. Just don't let it slip into logic that should be pure
ES2023 added toSorted(), toReversed() and other non-mutating array methods. Use them when you can!
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)
```javascript
// Pure: follows both rules
function multiply(a, b) {
return a * b
}
```
**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:
```javascript
function greet(name, time) {
return `Hello, ${name}! The time is ${time}`
}
```
**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:
```javascript
function addToCart(cart, item) {
return [...cart, item]
}
```
Use `structuredClone()` for a deep copy, or carefully spread at each level:
```javascript
// 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.
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
```javascript
// Testing a pure function - simple and straightforward
expect(add(2, 3)).toBe(5)
expect(formatName(' ALICE ')).toBe('alice')
expect(isValidEmail('[email protected]')).toBe(true)
```
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.