Back to Blog
Functional ProgrammingJavaScriptBest PracticesPatterns

Functional Programming Patterns in JavaScript

Write cleaner, more predictable code with functional programming. From pure functions to composition to practical patterns.

B
Bootspring Team
Engineering
January 10, 2025
6 min read

Functional programming emphasizes immutability, pure functions, and composition. These patterns lead to code that's easier to test, debug, and reason about.

Core Concepts#

Pure Functions#

1// ❌ Impure - depends on external state 2let taxRate = 0.1; 3function calculateTax(amount) { 4 return amount * taxRate; // External dependency 5} 6 7// ❌ Impure - has side effects 8function addToCart(cart, item) { 9 cart.push(item); // Mutates input 10 return cart; 11} 12 13// ✅ Pure - same input always gives same output 14function calculateTax(amount, taxRate) { 15 return amount * taxRate; 16} 17 18// ✅ Pure - no mutation 19function addToCart(cart, item) { 20 return [...cart, item]; // Returns new array 21}

Immutability#

1// ❌ Mutation 2const user = { name: 'John', age: 30 }; 3user.age = 31; // Mutates original 4 5// ✅ Immutable update 6const user = { name: 'John', age: 30 }; 7const updatedUser = { ...user, age: 31 }; // New object 8 9// ✅ Nested immutable update 10const state = { 11 user: { name: 'John', settings: { theme: 'dark' } } 12}; 13 14const newState = { 15 ...state, 16 user: { 17 ...state.user, 18 settings: { 19 ...state.user.settings, 20 theme: 'light' 21 } 22 } 23}; 24 25// Using Immer for complex updates 26import { produce } from 'immer'; 27 28const newState = produce(state, draft => { 29 draft.user.settings.theme = 'light'; 30});

First-Class Functions#

1// Functions as values 2const add = (a, b) => a + b; 3const multiply = (a, b) => a * b; 4 5// Functions as arguments 6function calculate(operation, a, b) { 7 return operation(a, b); 8} 9 10calculate(add, 2, 3); // 5 11calculate(multiply, 2, 3); // 6 12 13// Functions as return values 14function createMultiplier(factor) { 15 return (number) => number * factor; 16} 17 18const double = createMultiplier(2); 19const triple = createMultiplier(3); 20 21double(5); // 10 22triple(5); // 15

Higher-Order Functions#

Map, Filter, Reduce#

1const numbers = [1, 2, 3, 4, 5]; 2 3// Map: transform each element 4const doubled = numbers.map(n => n * 2); 5// [2, 4, 6, 8, 10] 6 7// Filter: select elements 8const evens = numbers.filter(n => n % 2 === 0); 9// [2, 4] 10 11// Reduce: combine to single value 12const sum = numbers.reduce((acc, n) => acc + n, 0); 13// 15 14 15// Chaining 16const result = numbers 17 .filter(n => n % 2 === 0) 18 .map(n => n * 2) 19 .reduce((acc, n) => acc + n, 0); 20// 12 (2*2 + 4*2)

Custom Higher-Order Functions#

1// Retry wrapper 2function withRetry(fn, maxRetries = 3) { 3 return async (...args) => { 4 let lastError; 5 for (let i = 0; i < maxRetries; i++) { 6 try { 7 return await fn(...args); 8 } catch (error) { 9 lastError = error; 10 await delay(Math.pow(2, i) * 1000); 11 } 12 } 13 throw lastError; 14 }; 15} 16 17const fetchWithRetry = withRetry(fetch); 18await fetchWithRetry('https://api.example.com/data'); 19 20// Memoization 21function memoize(fn) { 22 const cache = new Map(); 23 return (...args) => { 24 const key = JSON.stringify(args); 25 if (cache.has(key)) { 26 return cache.get(key); 27 } 28 const result = fn(...args); 29 cache.set(key, result); 30 return result; 31 }; 32} 33 34const expensiveCalculation = memoize((n) => { 35 console.log('Calculating...'); 36 return n * n; 37}); 38 39expensiveCalculation(5); // Calculating... 25 40expensiveCalculation(5); // 25 (cached)

Function Composition#

Pipe and Compose#

1// Pipe: left to right 2const pipe = (...fns) => (value) => 3 fns.reduce((acc, fn) => fn(acc), value); 4 5// Compose: right to left 6const compose = (...fns) => (value) => 7 fns.reduceRight((acc, fn) => fn(acc), value); 8 9// Usage 10const addOne = x => x + 1; 11const double = x => x * 2; 12const square = x => x * x; 13 14const calculate = pipe(addOne, double, square); 15calculate(2); // ((2 + 1) * 2)² = 36 16 17const calculate2 = compose(square, double, addOne); 18calculate2(2); // Same result: 36

Practical Composition#

1// Data transformation pipeline 2const processUser = pipe( 3 validateUser, 4 normalizeEmail, 5 hashPassword, 6 addTimestamps, 7 saveToDatabase 8); 9 10// String processing 11const slugify = pipe( 12 str => str.toLowerCase(), 13 str => str.trim(), 14 str => str.replace(/\s+/g, '-'), 15 str => str.replace(/[^a-z0-9-]/g, '') 16); 17 18slugify(' Hello World! '); // 'hello-world'

Currying and Partial Application#

Currying#

1// Manual currying 2const add = a => b => c => a + b + c; 3add(1)(2)(3); // 6 4 5// Curry helper 6const curry = (fn) => { 7 const arity = fn.length; 8 return function curried(...args) { 9 if (args.length >= arity) { 10 return fn(...args); 11 } 12 return (...more) => curried(...args, ...more); 13 }; 14}; 15 16const curriedAdd = curry((a, b, c) => a + b + c); 17curriedAdd(1)(2)(3); // 6 18curriedAdd(1, 2)(3); // 6 19curriedAdd(1)(2, 3); // 6 20curriedAdd(1, 2, 3); // 6

Partial Application#

1// Create specialized functions 2const multiply = (a, b) => a * b; 3const double = multiply.bind(null, 2); 4const triple = multiply.bind(null, 3); 5 6double(5); // 10 7triple(5); // 15 8 9// Partial application helper 10const partial = (fn, ...presetArgs) => 11 (...laterArgs) => fn(...presetArgs, ...laterArgs); 12 13const greet = (greeting, name) => `${greeting}, ${name}!`; 14const sayHello = partial(greet, 'Hello'); 15const sayGoodbye = partial(greet, 'Goodbye'); 16 17sayHello('John'); // 'Hello, John!' 18sayGoodbye('John'); // 'Goodbye, John!'

Functors and Monads#

Maybe (Handling Null)#

1class Maybe { 2 constructor(value) { 3 this.value = value; 4 } 5 6 static of(value) { 7 return new Maybe(value); 8 } 9 10 isNothing() { 11 return this.value === null || this.value === undefined; 12 } 13 14 map(fn) { 15 return this.isNothing() ? Maybe.of(null) : Maybe.of(fn(this.value)); 16 } 17 18 flatMap(fn) { 19 return this.isNothing() ? Maybe.of(null) : fn(this.value); 20 } 21 22 getOrElse(defaultValue) { 23 return this.isNothing() ? defaultValue : this.value; 24 } 25} 26 27// Usage 28const user = { profile: { address: { city: 'NYC' } } }; 29 30// Without Maybe (defensive coding) 31const city = user && user.profile && user.profile.address 32 ? user.profile.address.city 33 : 'Unknown'; 34 35// With Maybe 36const city = Maybe.of(user) 37 .map(u => u.profile) 38 .map(p => p.address) 39 .map(a => a.city) 40 .getOrElse('Unknown');

Either (Error Handling)#

1class Either { 2 constructor(value, isRight = true) { 3 this.value = value; 4 this.isRight = isRight; 5 } 6 7 static right(value) { 8 return new Either(value, true); 9 } 10 11 static left(value) { 12 return new Either(value, false); 13 } 14 15 map(fn) { 16 return this.isRight ? Either.right(fn(this.value)) : this; 17 } 18 19 flatMap(fn) { 20 return this.isRight ? fn(this.value) : this; 21 } 22 23 fold(leftFn, rightFn) { 24 return this.isRight ? rightFn(this.value) : leftFn(this.value); 25 } 26} 27 28// Usage 29function divide(a, b) { 30 return b === 0 31 ? Either.left('Cannot divide by zero') 32 : Either.right(a / b); 33} 34 35divide(10, 2) 36 .map(result => result * 2) 37 .fold( 38 error => console.error(error), 39 result => console.log(result) // 10 40 ); 41 42divide(10, 0) 43 .map(result => result * 2) // Skipped 44 .fold( 45 error => console.error(error), // 'Cannot divide by zero' 46 result => console.log(result) 47 );

Practical Patterns#

Point-Free Style#

1// With points (explicit parameters) 2const getNames = users => users.map(user => user.name); 3 4// Point-free (no explicit parameters) 5const prop = key => obj => obj[key]; 6const getNames = users => users.map(prop('name')); 7 8// Or with composition 9const getName = prop('name'); 10const getNames = map(getName);

Pipeline Operator (Proposal)#

1// Future JavaScript (stage 2 proposal) 2const result = value 3 |> addOne 4 |> double 5 |> square; 6 7// Currently, use pipe function 8const result = pipe(addOne, double, square)(value);

Conclusion#

Functional programming patterns make code more predictable and testable. Start with pure functions and immutability, then gradually adopt composition and higher-order functions.

You don't need to go full functional—even partial adoption improves code quality. Use these patterns where they make sense, and keep code readable.

Share this article

Help spread the word about Bootspring