Pure Functions and Side Effects

· 7 min read · Updated March 27, 2026 · beginner
javascript functional-programming pure-functions

A pure function is just a function that gives you the same result for the same input, and doesn’t change anything outside itself. No special tools required. Once you understand this, you’ll start noticing where your own code follows these rules and where it doesn’t.

What Makes a Function Pure?

A function is pure when it satisfies two conditions:

  1. Same input always produces the same output. Call it with (2, 3) and it returns 5 every single time, forever.
  2. No side effects. The function doesn’t modify global state, the DOM, the console, or anything else outside of itself.

Here’s a pure function:

function add(a, b) {
  return a + b;
}

Call add(2, 3) once, you get 5. Call it a thousand times, you get 5 every time. Nothing else in your program changes because of this function.

Now compare that to an impure version:

let total = 0;

function addToTotal(value) {
  total += value;
  return total;
}

The first call addToTotal(2) returns 2. The second call with the same argument returns 4. The output changed even though the input didn’t. That’s a telltale sign of impurity — and it makes the function harder to reason about.

Common Side Effects in JavaScript

A side effect is any change your function causes that goes beyond its return value. Here are the side effects you’ll run into most often in JavaScript code.

Mutating Objects or Arrays

JavaScript passes objects and arrays by reference. If your function modifies the original array, that’s a side effect.

// Impure — mutates the original array
function addItemImpure(arr, item) {
  arr.push(item);
  return arr;
}

// Pure — creates a new array
function addItemPure(arr, item) {
  return [...arr, item];
}

With addItemImpure, the original array is changed for everyone using it. With addItemPure, the original stays intact and you get a new one back.

Console Output

console.log is a side effect. Any function that calls it is impure, even if it also returns a value.

// Impure
function greetImpure(name) {
  console.log(`Hello, ${name}!`);
  return `Hello, ${name}!`;
}

// Pure — no side effects
function greetPure(name) {
  return `Hello, ${name}!`;
}

Utility functions that log to the console are a common source of hidden impurity. In a small script this doesn’t matter much, but in a larger codebase it makes testing harder because every log call is an unexpected interaction with the outside world.

Modifying Global State

Changing a variable that lives outside your function’s scope is a side effect.

// Impure
let multiplier = 2;

function multiplyImpure(n) {
  return n * multiplier;
}

// Pure — multiplier is passed in as an argument
function multiplyPure(n, multiplier) {
  return n * multiplier;
}

The impure version depends on something no one told you about. Change multiplier somewhere else in your code and multiplyImpure suddenly returns different results. The pure version eliminates this hidden dependency.

DOM Manipulation

When a function reaches out and changes the page, that’s a side effect.

// Impure — modifies the DOM
function updateTitleImpure(text) {
  document.title = text;
}

// Pure — just returns the value
function updateTitlePure(text) {
  return text;
}

The pure version is easy to test. You call it, check the return value, and you’re done. The impure version requires a browser environment with a real DOM to test properly.

Randomness

Math.random() produces a different value on every call. Any function that uses it is therefore impure.

// Impure — returns a different greeting each time
function randomGreetingImpure() {
  const greetings = ['Hi', 'Hello', 'Hey'];
  return greetings[Math.floor(Math.random() * greetings.length)];
}

The same call to this function should always return the same result if it were pure. Instead, you get something different every time.

Pure vs. Impure: Side-by-Side Examples

Doubling Numbers

// Impure — mutates the original array
function doubleImpure(nums) {
  for (let i = 0; i < nums.length; i++) {
    nums[i] *= 2;
  }
  return nums;
}

// Pure — returns a new array
function doublePure(nums) {
  return nums.map(n => n * 2);
}

If you pass [1, 2, 3] to doubleImpure, the original array is now [2, 4, 6]. Call it again on the same array and you get [4, 8, 12]. That’s the same input producing different outputs over time.

doublePure always returns [2, 4, 6] for [1, 2, 3]. The original is untouched.

Finding a User

// Impure — depends on external mutable state
let users = [{ id: 1, name: 'Alice' }];

function findUserImpure(id) {
  return users.find(u => u.id === id);
}

// Pure — takes the data as an argument
function findUserPure(users, id) {
  return users.find(u => u.id === id);
}

The impure version works today, but if someone reassigns users elsewhere in the code, it breaks. The pure version is honest about what it needs — it asks for the data explicitly.

Why This Matters

Easier Testing

Pure functions are straightforward to test. You pass in arguments, check the return value. No setup, no mocks, no teardown.

expect(doublePure([1, 2, 3])).toEqual([2, 4, 6]);
expect(doublePure([1, 2, 3])).toEqual([2, 4, 6]); // always passes

Run this test a hundred times and it does the same thing every time. That’s not true for tests against impure functions, which often need complex setup to get the external state just right.

Predictability

When a function is pure, you never get a surprise. Call it with the same arguments and the same result comes back. This removes an entire class of bugs where code “works” in one place but fails in another depending on what ran before.

Memoization

Because a pure function’s output depends only on its inputs, you can cache the result and return it instantly on the next call with the same arguments.

function memoize(fn) {
  const cache = new Map();
  return function(...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key);
    const result = fn(...args);
    cache.set(key, result);
    return result;
  };
}

Impure functions can’t be safely memoized. The cached result might be wrong if something else changed the function’s behavior between calls.

Parallel Execution

Pure functions don’t share state with each other, which means you can run multiple pure function calls at the same time without them interfering. This matters in JavaScript because modern runtimes can execute async operations concurrently, and pure functions are safe in those environments.

Pure Functions in Real Code

You don’t need to make every function pure. Applications need side effects — network requests, DOM updates, logging. Those are how your program talks to the outside world.

The idea is to keep your core logic pure while reserving side effects for the outer edges of your application. Your data transformation functions, your utility helpers, your business logic — those are the places where purity pays off with easier testing and fewer surprises.

// Pure core — easy to test
function calculateTotal(items) {
  return items.reduce((sum, item) => sum + item.price, 0);
}

// Impure boundary — handles the side effect
async function placeOrder(items) {
  const total = calculateTotal(items); // pure
  const response = await fetch('/api/orders', { // side effect
    method: 'POST',
    body: JSON.stringify({ items, total })
  });
  return response.json();
}

The pure calculateTotal function is trivial to unit test. The impure placeOrder handles the network call, but most of your application logic lives in functions that don’t need to worry about side effects.

Conclusion

Pure functions are not a theoretical concept. They’re a practical tool that makes code easier to test, debug, and reason about. A function is pure when it always returns the same output for the same input and doesn’t modify anything outside itself. Side effects like console calls, DOM changes, and global state mutations make functions impure.

You won’t make everything pure — and that’s fine. But moving the heart of your logic into pure functions while keeping side effects at the boundaries of your application will make your codebase dramatically easier to work with.

See Also

  • JavaScript Closures — closures let you encapsulate state privately, which pairs well with pure functions
  • JavaScript Event Loop — understanding how JavaScript handles async operations helps explain why pure functions matter in concurrent code
  • JavaScript Modules and ESM — modules encourage isolated, self-contained code, which is easier to keep pure