What is a pure function in JavaScript?

👨‍💻 Frontend Developer 🟡 Often Asked 🎚️ Easy
#JavaScript #JS Basics #Functions

Quick Answer

A pure function — is a function that:

  1. Always returns the same result for the same input data
  2. Has no side effects (doesn’t change external state)
  3. Doesn’t depend on external state (only on its parameters)
// Pure function
function add(a, b) {
  return a + b; // Always the same result for the same a and b
}
 
// Impure function
let counter = 0;
function increment() {
  return ++counter; // Depends on external variable
}

What is a Pure Function

A Pure Function — is a fundamental concept of functional programming that defines a function as a “mathematical” operation. Such functions are the foundation of predictable and reliable code.

Core Principles of Pure Functions

  1. Determinism — the same input data always produces the same result
  2. No side effects — the function doesn’t change anything outside its scope
  3. Referential transparency — a function call can be replaced with its result without changing program behavior

Determinism: Same Input — Same Output

✅ Examples of Pure Functions

// Mathematical operations
function multiply(a, b) {
  return a * b;
}
 
// String operations
function capitalize(str) {
  return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
}
 
// Array operations (without mutation)
function filterEvenNumbers(numbers) {
  return numbers.filter(num => num % 2 === 0);
}
 
// Object operations (without mutation)
function updateUserAge(user, newAge) {
  return { ...user, age: newAge };
}
 
// Complex calculations
function calculateTax(price, taxRate) {
  return price * (taxRate / 100);
}

❌ Examples of Impure Functions

// Depends on current time
function getCurrentTimestamp() {
  return Date.now(); // Each call gives a different result
}
 
// Depends on external variable
let discount = 0.1;
function applyDiscount(price) {
  return price * (1 - discount); // Result depends on external variable
}
 
// Uses random numbers
function generateRandomId() {
  return Math.random().toString(36); // Always different result
}
 
// Depends on DOM
function getElementWidth(elementId) {
  return document.getElementById(elementId).offsetWidth;
}

No Side Effects

What Are Side Effects

Side effects — are any changes to program state or interaction with the external world:

  • Modifying global variables
  • Mutating passed objects/arrays
  • Console or screen output
  • Server requests
  • DOM modifications
  • File writing
  • Database state changes

✅ Functions Without Side Effects

// Returns new array without modifying the original
function addItemToArray(array, item) {
  return [...array, item];
}
 
// Returns new object without modifying the original
function updateUserProfile(user, updates) {
  return {
    ...user,
    ...updates,
    updatedAt: new Date().toISOString()
  };
}
 
// Calculations without changing external state
function calculateMonthlyPayment(principal, rate, months) {
  const monthlyRate = rate / 12 / 100;
  const payment = principal * 
    (monthlyRate * Math.pow(1 + monthlyRate, months)) /
    (Math.pow(1 + monthlyRate, months) - 1);
  return Math.round(payment * 100) / 100;
}

❌ Functions With Side Effects

// Modifies global variable
let totalSales = 0;
function addSale(amount) {
  totalSales += amount; // Side effect!
  return totalSales;
}
 
// Modifies passed object
function updateUser(user, newData) {
  user.name = newData.name; // Mutation!
  user.updatedAt = Date.now();
  return user;
}
 
// Console output
function debugCalculation(a, b) {
  const result = a + b;
  console.log(`${a} + ${b} = ${result}`); // Side effect!
  return result;
}
 
// Modifies DOM
function updateCounter(value) {
  document.getElementById('counter').textContent = value; // Side effect!
  return value;
}

Advantages of Pure Functions

1. Predictability and Reliability

// Pure function - always predictable result
function formatPrice(price, currency = 'USD') {
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: currency
  }).format(price);
}
 
// Can be confident in the result
console.log(formatPrice(1000)); // "$1,000.00"
console.log(formatPrice(1000)); // "$1,000.00" - always the same

2. Easy Testing

// Pure function is easy to test
function calculateDiscount(price, discountPercent) {
  if (discountPercent < 0 || discountPercent > 100) {
    throw new Error('Discount must be between 0 and 100%');
  }
  return price * (discountPercent / 100);
}
 
// Simple tests
function testCalculateDiscount() {
  console.assert(calculateDiscount(1000, 10) === 100);
  console.assert(calculateDiscount(500, 20) === 100);
  console.assert(calculateDiscount(0, 50) === 0);
  
  try {
    calculateDiscount(1000, -5);
    console.assert(false, 'Should throw an error');
  } catch (e) {
    console.assert(e.message.includes('Discount must be'));
  }
}

3. Memoization Capability

// Pure functions can be memoized
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.apply(this, args);
    cache.set(key, result);
    return result;
  };
}
 
// Expensive pure function
function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}
 
// Memoized version
const memoizedFibonacci = memoize(fibonacci);
 
console.log(memoizedFibonacci(40)); // Calculated
console.log(memoizedFibonacci(40)); // Retrieved from cache

4. Parallel Execution

// Pure functions are safe for parallel execution
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
 
// Pure function for processing
function processNumber(num) {
  return num * num + Math.sqrt(num);
}
 
// Can safely use in Promise.all
const promises = numbers.map(num => 
  Promise.resolve(processNumber(num))
);
 
Promise.all(promises).then(results => {
  console.log(results); // Result is predictable
});

Practical Examples

Data Validation

// Pure validation functions
function isValidEmail(email) {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(email);
}
 
function isValidPassword(password) {
  return password.length >= 8 && 
         /[A-Z]/.test(password) && 
         /[a-z]/.test(password) && 
         /[0-9]/.test(password);
}
 
function validateUser(userData) {
  const errors = [];
  
  if (!userData.email || !isValidEmail(userData.email)) {
    errors.push('Invalid email');
  }
  
  if (!userData.password || !isValidPassword(userData.password)) {
    errors.push('Password must contain at least 8 characters, including uppercase and lowercase letters, digits');
  }
  
  return {
    isValid: errors.length === 0,
    errors
  };
}

Data Transformation

// Pure functions for data processing
function normalizeUser(rawUser) {
  return {
    id: rawUser.id,
    name: rawUser.full_name?.trim() || '',
    email: rawUser.email?.toLowerCase() || '',
    age: parseInt(rawUser.age) || 0,
    isActive: rawUser.status === 'active'
  };
}
 
function groupUsersByAge(users) {
  return users.reduce((groups, user) => {
    const ageGroup = Math.floor(user.age / 10) * 10;
    const groupKey = `${ageGroup}-${ageGroup + 9}`;
    
    if (!groups[groupKey]) {
      groups[groupKey] = [];
    }
    
    groups[groupKey].push(user);
    return groups;
  }, {});
}
 
function calculateUserStats(users) {
  return {
    total: users.length,
    active: users.filter(user => user.isActive).length,
    averageAge: users.reduce((sum, user) => sum + user.age, 0) / users.length,
    ageGroups: groupUsersByAge(users)
  };
}

Functional Utilities

// Composition of pure functions
function pipe(...functions) {
  return function(value) {
    return functions.reduce((acc, fn) => fn(acc), value);
  };
}
 
function compose(...functions) {
  return function(value) {
    return functions.reduceRight((acc, fn) => fn(acc), value);
  };
}
 
// Pure functions for string processing
function trim(str) {
  return str.trim();
}
 
function toLowerCase(str) {
  return str.toLowerCase();
}
 
function removeSpaces(str) {
  return str.replace(/\s+/g, '');
}
 
// Function composition
const normalizeString = pipe(
  trim,
  toLowerCase,
  removeSpaces
);
 
console.log(normalizeString('  Hello World  ')); // "helloworld"

Working with State in Pure Functions

Immutable Array Operations

// Adding an element
function addItem(array, item) {
  return [...array, item];
}
 
// Removing element by index
function removeItemByIndex(array, index) {
  return array.filter((_, i) => i !== index);
}
 
// Updating an element
function updateItem(array, index, newItem) {
  return array.map((item, i) => i === index ? newItem : item);
}
 
// Sorting (doesn't modify original array)
function sortBy(array, keyFn) {
  return [...array].sort((a, b) => {
    const aKey = keyFn(a);
    const bKey = keyFn(b);
    return aKey < bKey ? -1 : aKey > bKey ? 1 : 0;
  });
}
 
// Grouping
function groupBy(array, keyFn) {
  return array.reduce((groups, item) => {
    const key = keyFn(item);
    if (!groups[key]) {
      groups[key] = [];
    }
    groups[key].push(item);
    return groups;
  }, {});
}

Immutable Object Operations

// Updating object property
function updateProperty(obj, key, value) {
  return { ...obj, [key]: value };
}
 
// Removing property
function removeProperty(obj, key) {
  const { [key]: removed, ...rest } = obj;
  return rest;
}
 
// Deep update of nested object
function updateNestedProperty(obj, path, value) {
  const [head, ...tail] = path;
  
  if (tail.length === 0) {
    return { ...obj, [head]: value };
  }
  
  return {
    ...obj,
    [head]: updateNestedProperty(obj[head] || {}, tail, value)
  };
}
 
// Merging objects
function mergeObjects(...objects) {
  return Object.assign({}, ...objects);
}
 
// Usage example
const user = {
  id: 1,
  profile: {
    name: 'John',
    settings: {
      theme: 'dark',
      notifications: true
    }
  }
};
 
const updatedUser = updateNestedProperty(
  user, 
  ['profile', 'settings', 'theme'], 
  'light'
);

When You Can’t Use Pure Functions

Operations with Side Effects

// These operations are impure by definition
 
// Logging
function logError(error) {
  console.error('Error:', error.message);
  // Sending to monitoring system
  errorTracker.send(error);
}
 
// DOM manipulation
function updateUI(data) {
  document.getElementById('content').innerHTML = data.html;
  document.title = data.title;
}
 
// HTTP requests
async function fetchUserData(userId) {
  const response = await fetch(`/api/users/${userId}`);
  return response.json();
}
 
// localStorage operations
function saveToStorage(key, value) {
  localStorage.setItem(key, JSON.stringify(value));
}

Isolating Side Effects

// Separating pure logic and side effects
 
// Pure function for data preparation
function prepareUserData(rawData) {
  return {
    id: rawData.id,
    name: rawData.name.trim(),
    email: rawData.email.toLowerCase(),
    isValid: isValidEmail(rawData.email)
  };
}
 
// Impure function for saving
function saveUser(userData) {
  // Side effect is isolated
  return fetch('/api/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(userData)
  });
}
 
// Composition: pure preparation + impure saving
async function createUser(rawData) {
  const userData = prepareUserData(rawData);
  
  if (!userData.isValid) {
    throw new Error('Invalid user data');
  }
  
  return await saveUser(userData);
}

Comparison: Pure vs Impure Functions

AspectPure FunctionsImpure Functions
PredictabilityAlways the same resultResult may vary
TestingEasy to testRequire mocks and stubs
DebuggingSimple debuggingComplex debugging
CachingCan be memoizedCannot be cached
ParallelismSafe for parallel executionMay cause race conditions
RefactoringEasy to refactorHard to refactor
Code UnderstandingEasy to understand logicNeed to consider context
ReusabilityHigh reusabilityLimited reusability

Best Practices

1. Strive for Purity

// ❌ Bad: mixing pure logic and side effects
function processAndSaveUser(userData) {
  // Pure logic
  const normalizedData = {
    name: userData.name.trim(),
    email: userData.email.toLowerCase()
  };
  
  // Side effect
  console.log('Processing user:', normalizedData.name);
  
  // Another side effect
  database.save(normalizedData);
  
  return normalizedData;
}
 
// ✅ Good: separation of concerns
function normalizeUserData(userData) {
  return {
    name: userData.name.trim(),
    email: userData.email.toLowerCase()
  };
}
 
function logUserProcessing(userData) {
  console.log('Processing user:', userData.name);
}
 
function saveUserToDatabase(userData) {
  return database.save(userData);
}
 
// Composition
function processUser(userData) {
  const normalizedData = normalizeUserData(userData);
  logUserProcessing(normalizedData);
  return saveUserToDatabase(normalizedData);
}

2. Use Immutability

// ❌ Bad: data mutation
function addTodoItem(todos, newItem) {
  todos.push(newItem); // Modifies original array
  return todos;
}
 
// ✅ Good: immutable addition
function addTodoItem(todos, newItem) {
  return [...todos, newItem]; // Returns new array
}
 
// ❌ Bad: object mutation
function updateUserProfile(user, updates) {
  user.name = updates.name; // Modifies original object
  user.updatedAt = Date.now();
  return user;
}
 
// ✅ Good: immutable update
function updateUserProfile(user, updates) {
  return {
    ...user,
    ...updates,
    updatedAt: Date.now()
  };
}

3. Avoid Hidden Dependencies

// ❌ Bad: hidden dependency on global variable
const TAX_RATE = 0.18;
 
function calculateTotalPrice(price) {
  return price * (1 + TAX_RATE); // Depends on global variable
}
 
// ✅ Good: explicit dependency passing
function calculateTotalPrice(price, taxRate) {
  return price * (1 + taxRate);
}
 
// Or with default value
function calculateTotalPrice(price, taxRate = 0.18) {
  return price * (1 + taxRate);
}

4. Document Function Purity

/**
 * Calculates final cost including discount and tax
 * @pure
 * @param {number} price - Base price
 * @param {number} discountPercent - Discount percentage (0-100)
 * @param {number} taxRate - Tax rate (0-1)
 * @returns {number} Final cost
 */
function calculateFinalPrice(price, discountPercent, taxRate) {
  const discountAmount = price * (discountPercent / 100);
  const discountedPrice = price - discountAmount;
  return discountedPrice * (1 + taxRate);
}
 
/**
 * Formats user name
 * @pure
 * @param {string} firstName - First name
 * @param {string} lastName - Last name
 * @returns {string} Formatted full name
 */
function formatUserName(firstName, lastName) {
  return `${firstName.trim()} ${lastName.trim()}`;
}

Common Mistakes

1. Hidden Mutation

Problem: Function appears pure but mutates input data

// Seems pure but mutates the array
function sortUsers(users) {
  return users.sort((a, b) => a.name.localeCompare(b.name));
}
 
// Array.sort() modifies the original array!
const originalUsers = [{ name: 'Bob' }, { name: 'Anna' }];
const sortedUsers = sortUsers(originalUsers);
console.log(originalUsers); // Also sorted!

Solution: Create a copy before mutating operations

function sortUsers(users) {
  return [...users].sort((a, b) => a.name.localeCompare(b.name));
}

2. Time Dependency

// ❌ Bad: depends on current time
function createTimestamp() {
  return Date.now();
}
 
function isWorkingHours() {
  const hour = new Date().getHours();
  return hour >= 9 && hour <= 18;
}
 
// ✅ Good: accepts time as parameter
function createTimestamp(date = new Date()) {
  return date.getTime();
}
 
function isWorkingHours(date = new Date()) {
  const hour = date.getHours();
  return hour >= 9 && hour <= 18;
}

3. Hidden Side Effects

// ❌ Bad: hidden logging
function calculateDiscount(price, percent) {
  console.log(`Calculating discount: ${price} * ${percent}%`); // Side effect!
  return price * (percent / 100);
}
 
// ✅ Good: pure function + separate logging
function calculateDiscount(price, percent) {
  return price * (percent / 100);
}
 
function calculateDiscountWithLogging(price, percent) {
  const result = calculateDiscount(price, percent);
  console.log(`Calculating discount: ${price} * ${percent}% = ${result}`);
  return result;
}

Testing Pure Functions

Simplicity of Testing

// Pure function
function validatePassword(password) {
  const errors = [];
  
  if (password.length < 8) {
    errors.push('Minimum 8 characters');
  }
  
  if (!/[A-Z]/.test(password)) {
    errors.push('Need uppercase letter');
  }
  
  if (!/[0-9]/.test(password)) {
    errors.push('Need digit');
  }
  
  return {
    isValid: errors.length === 0,
    errors
  };
}
 
// Simple tests
function testValidatePassword() {
  // Test valid password
  const validResult = validatePassword('Password123');
  console.assert(validResult.isValid === true);
  console.assert(validResult.errors.length === 0);
  
  // Test short password
  const shortResult = validatePassword('Pass1');
  console.assert(shortResult.isValid === false);
  console.assert(shortResult.errors.includes('Minimum 8 characters'));
  
  // Test without uppercase
  const noUpperResult = validatePassword('password123');
  console.assert(noUpperResult.isValid === false);
  console.assert(noUpperResult.errors.includes('Need uppercase letter'));
  
  // Test without digit
  const noDigitResult = validatePassword('Password');
  console.assert(noDigitResult.isValid === false);
  console.assert(noDigitResult.errors.includes('Need digit'));
  
  console.log('All tests passed!');
}
 
testValidatePassword();

Property-Based Testing

// Testing properties of pure functions
function testMathProperties() {
  // Commutativity of addition
  for (let i = 0; i < 100; i++) {
    const a = Math.random() * 1000;
    const b = Math.random() * 1000;
    console.assert(add(a, b) === add(b, a));
  }
  
  // Associativity of addition
  for (let i = 0; i < 100; i++) {
    const a = Math.random() * 1000;
    const b = Math.random() * 1000;
    const c = Math.random() * 1000;
    console.assert(add(add(a, b), c) === add(a, add(b, c)));
  }
  
  console.log('Mathematical properties verified!');
}
 
function add(a, b) {
  return a + b;
}

Conclusion

Key Advantages of Pure Functions

  1. Predictability — same input data always produces the same result
  2. Testability — easy to write and maintain tests
  3. Debugging — easier to find and fix bugs
  4. Reusability — can be used in different contexts
  5. Parallelism — safe for multi-threaded execution
  6. Optimization — possibility of memoization and other optimizations

When to Use Pure Functions

  • Always, when possible
  • For business logic and calculations
  • When processing data
  • For validation and formatting
  • In utility functions
  • When working with immutable data structures
  • Functional programming is becoming more popular
  • Immutability as the foundation of reliable code
  • Redux and other state libraries are based on pure functions
  • React encourages the use of pure components
  • TypeScript helps ensure type safety of pure functions

Remember: pure functions are not just a good practice, they are the foundation of quality, maintainable and reliable code. Strive for function purity wherever possible, and isolate side effects in separate functions.


Want more articles on interview preparation?
Follow EasyAdvice, bookmark the site, and level up every day 💪