When I first started learning JavaScript, I mostly focused on what the
code should do.
If I needed to change an array, I would change the array.\
If I needed to reuse some logic, I would put it inside a function.\
If I needed to process data, I would probably reach for a for loop.
There is nothing wrong with that approach. But as applications become
larger, the way we structure our logic starts to matter just as much as
the result.
This is where Functional Programming (FP) becomes useful.
Functional programming is not about avoiding every loop or writing
complicated one-line expressions. It is mainly about making our code
predictable, reusable, and easier to reason about.
In this blog, I'll walk through some of the most useful functional
programming concepts in JavaScript:
- Pure functions
- Immutability
- Higher-order functions
-
map(),filter(), andreduce() - Currying
- Composition with
pipe()andcompose()
1. What Is Functional Programming?
Functional programming is a programming style where we try to solve
problems by combining functions and transforming data.
Instead of thinking:
"First change this variable, then change that variable, then update
another variable."
we can think:
"Take this data, transform it, and pass the result to the next
function."
For example:
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(number => number * 2);
console.log(doubled);
// [2, 4, 6, 8, 10]
The original numbers array is still there. We created a new result
from it.
That small idea is at the heart of a lot of functional programming.
2. Pure Functions
A pure function is a function that behaves predictably.
A function is considered pure when:
- The same input always produces the same output.
- The function does not create side effects outside itself.
Example of a pure function
function add(a, b) {
return a + b;
}
console.log(add(2, 3));
// 5
console.log(add(2, 3));
// 5
Every time we call add(2, 3), we get 5.
The function does not depend on some outside variable that might
suddenly change.
Another example
function square(number) {
return number * number;
}
This is easy to understand:
square(4); // 16
square(10); // 100
We know exactly what the function will return.
Impure functions
Now consider this:
let total = 10;
function addToTotal(value) {
total += value;
return total;
}
This function changes the external variable total.
So the result depends on what happened before:
addToTotal(5); // 15
addToTotal(5); // 20
The same argument 5 did not produce the same result.
This makes the function harder to reason about.
Why pure functions are useful
Pure functions make code:
- Easier to test
- Easier to debug
- Easier to reuse
- Easier to understand
- Safer to combine with other functions
A useful mental model is:
input → function → output
The fewer hidden dependencies a function has, the easier it is to work
with.
3. Immutability
Immutability means that instead of changing existing data, we create
new data.
Consider an array:
const numbers = [1, 2, 3];
We could modify it directly:
numbers.push(4);
Now the original array has changed:
console.log(numbers);
// [1, 2, 3, 4]
That is a mutation.
In functional programming, we often prefer:
const numbers = [1, 2, 3];
const updatedNumbers = [...numbers, 4];
console.log(numbers);
// [1, 2, 3]
console.log(updatedNumbers);
// [1, 2, 3, 4]
We kept the original data untouched and created a new array.
Immutability with objects
The same idea applies to objects.
Instead of:
const user = {
name: "Koushik",
age: 22
};
user.age = 23;
we can create a new object:
const user = {
name: "Koushik",
age: 22
};
const updatedUser = {
...user,
age: 23
};
console.log(user);
// { name: "Koushik", age: 22 }
console.log(updatedUser);
// { name: "Koushik", age: 23 }
This becomes particularly useful in applications where many parts of the
program depend on the same data.
When data is not unexpectedly modified, tracking changes becomes much
easier.
4. Higher-Order Functions
A higher-order function is a function that does at least one of
these:
- Takes another function as an argument
- Returns another function
JavaScript supports this because functions are treated as values.
For example:
function greet(name) {
return `Hello ${name}`;
}
function processUser(callback) {
return callback("Koushik");
}
console.log(processUser(greet));
// Hello Koushik
Here, processUser() receives another function.
So processUser is a higher-order function.
A function returning another function
This is also a higher-order function:
function multiplyBy(number) {
return function(value) {
return value * number;
};
}
const multiplyByTwo = multiplyBy(2);
console.log(multiplyByTwo(5));
// 10
The interesting part is:
multiplyBy(2)
doesn't immediately return a number.
It returns a new function.
That idea will become important when we talk about currying.
5. map(), filter(), and reduce()
Three of the most useful functional methods in JavaScript are:
map()
filter()
reduce()
They allow us to transform and process collections without manually
managing indexes.
map()
Use map() when you want to transform every item in an array.
For example:
const numbers = [1, 2, 3, 4];
const doubled = numbers.map(number => number * 2);
console.log(doubled);
// [2, 4, 6, 8]
Think of map() as:
one item → transform it → one new item
If the original array has 4 items, the resulting array normally also has
4 items.
Another example
const users = [
{ name: "Asha", age: 21 },
{ name: "Rahul", age: 25 },
{ name: "Maya", age: 23 }
];
const names = users.map(user => user.name);
console.log(names);
// ["Asha", "Rahul", "Maya"]
We transformed an array of objects into an array of names.
6. filter()
Use filter() when you want to keep only the items that satisfy a
condition.
Example:
const numbers = [1, 2, 3, 4, 5, 6];
const evenNumbers = numbers.filter(number => number % 2 === 0);
console.log(evenNumbers);
// [2, 4, 6]
The callback returns either:
true
or
false
If it returns true, the item stays.
If it returns false, the item is removed from the result.
Real-world example
const users = [
{ name: "Asha", active: true },
{ name: "Rahul", active: false },
{ name: "Maya", active: true }
];
const activeUsers = users.filter(user => user.active);
console.log(activeUsers);
This is much easier to read than manually creating another array and
pushing items into it.
7. reduce()
reduce() is useful when we want to combine many values into one
result.
For example, adding numbers:
const numbers = [10, 20, 30, 40];
const total = numbers.reduce(
(sum, number) => sum + number,
0
);
console.log(total);
// 100
The second argument:
0
is the initial value of the accumulator.
You can think about reduce() like this:
start with 0
↓
0 + 10 = 10
↓
10 + 20 = 30
↓
30 + 30 = 60
↓
60 + 40 = 100
reduce() can do more than addition
For example, we can create an object from an array:
const users = [
{ name: "Asha", role: "developer" },
{ name: "Rahul", role: "designer" },
{ name: "Maya", role: "developer" }
];
const usersByRole = users.reduce((result, user) => {
if (!result[user.role]) {
result[user.role] = [];
}
result[user.role].push(user.name);
return result;
}, {});
console.log(usersByRole);
The result becomes:
{
developer: ["Asha", "Maya"],
designer: ["Rahul"]
}
This shows why reduce() is powerful: it can transform a collection
into many different kinds of results.
8. map vs filter vs reduce
A simple way to remember them:
Method Main purpose Typical result
map() Transform every item New array
filter() Select certain items Smaller/new array
reduce() Combine items into a result Any value
For example:
const numbers = [1, 2, 3, 4, 5];
map()
numbers.map(n => n * 2);
Result:
[2, 4, 6, 8, 10]
filter()
numbers.filter(n => n > 2);
Result:
[3, 4, 5]
reduce()
numbers.reduce((sum, n) => sum + n, 0);
Result:
15
9. Currying
Currying sounds complicated at first, but the basic idea is simple.
Currying converts a function that takes multiple arguments into a
series of functions that each take one argument.
Instead of:
function add(a, b) {
return a + b;
}
add(2, 3);
we can write:
function add(a) {
return function(b) {
return a + b;
};
}
console.log(add(2)(3));
// 5
The first function receives 2 and returns another function.
That second function receives 3.
Currying with arrow functions
The same thing can be written more compactly:
const add = a => b => a + b;
console.log(add(2)(3));
// 5
It may look strange initially, but break it down:
add(2)
returns:
b => 2 + b
Then:
add(2)(3)
becomes:
2 + 3
which gives:
5
Why use currying?
Currying becomes useful when we want to create specialized functions.
For example:
const multiplyBy = a => b => a * b;
const multiplyBy10 = multiplyBy(10);
console.log(multiplyBy10(5));
// 50
console.log(multiplyBy10(8));
// 80
Instead of repeatedly passing 10, we created a reusable function that
already knows it should multiply by 10.
This idea is closely connected to function reuse and composition.
10. Function Composition
Function composition means taking small functions and combining them to
create a larger operation.
Imagine these functions:
const double = number => number * 2;
const addTen = number => number + 10;
We can manually combine them:
const result = addTen(double(5));
console.log(result);
// 20
The flow is:
5
↓
double
↓
10
↓
addTen
↓
20
This is composition.
Instead of putting all the logic into one large function, we keep each
operation small.
11. pipe()
A pipe() function allows us to write the same flow from left to
right.
Here is a simple implementation:
const pipe = (...functions) => value =>
functions.reduce((result, fn) => fn(result), value);
Now we can write:
const double = number => number * 2;
const addTen = number => number + 10;
const processNumber = pipe(
double,
addTen
);
console.log(processNumber(5));
// 20
The data flows like this:
5
↓
double
↓
10
↓
addTen
↓
20
I personally find pipe() easier to read because the execution order is
the same as the order in which the functions are written.
12. compose()
compose() is very similar to pipe(), but the functions are usually
applied right to left.
For example:
const compose = (...functions) => value =>
functions.reduceRight((result, fn) => fn(result), value);
Now:
const double = number => number * 2;
const addTen = number => number + 10;
const processNumber = compose(
addTen,
double
);
console.log(processNumber(5));
// 20
The execution is:
5
↓
double
↓
10
↓
addTen
↓
20
The important difference is how the functions are listed.
pipe()
pipe(double, addTen);
Reads:
double → addTen
compose()
compose(addTen, double);
Reads from right to left:
double → addTen
13. Putting Everything Together
Now let's combine several of these ideas.
Suppose we have a list of users:
const users = [
{ name: "Asha", age: 20, active: true },
{ name: "Rahul", age: 17, active: true },
{ name: "Maya", age: 25, active: false },
{ name: "Arjun", age: 30, active: true }
];
Our goal is:
- Keep active users.
- Keep users who are adults.
- Extract their names.
We can do that with:
const getActiveUsers = users =>
users.filter(user => user.active);
const getAdults = users =>
users.filter(user => user.age >= 18);
const getNames = users =>
users.map(user => user.name);
Now we can compose them:
const getActiveAdultNames = pipe(
getActiveUsers,
getAdults,
getNames
);
console.log(getActiveAdultNames(users));
Result:
["Asha", "Arjun"]
This is a good example of the functional programming mindset.
Instead of writing one large function that does everything, we created
several small functions.
Each function has one responsibility.
Then we connected them together.
14. Why This Style Is Useful
Functional programming can make applications easier to maintain because
small functions are easier to understand individually.
For example:
const getActiveUsers = users =>
users.filter(user => user.active);
This function has a very clear responsibility.
If something goes wrong with active-user filtering, we know where to
look.
The same idea becomes especially useful in:
- Data processing
- API response transformation
- State management
- Frontend applications
- Backend services
- Testing
- Complex business logic
It also encourages us to avoid unnecessary mutations and hidden side
effects.
15. A Practical Mental Model
When writing JavaScript, I find it useful to ask a few questions:
1. Can this function be pure?
Instead of depending on external variables:
function calculatePrice(price) {
return price * 1.18;
}
keep the required data as input when possible.
2. Do I really need to mutate this data?
Instead of:
items.push(newItem);
consider:
const updatedItems = [...items, newItem];
when immutability makes the code easier to reason about.
3. Can I reuse this logic?
If the same logic appears in several places, extract it into a function.
4. Can I transform the data with map/filter/reduce?
Instead of manually managing indexes and temporary variables, see
whether one of these methods expresses the intention more clearly.
5. Can I break a large operation into smaller functions?
For example:
fetchData
→ filterData
→ transformData
→ sortData
→ formatData
can often be represented as a pipeline.
16. Functional Programming Does Not Mean "Never Use Loops"
This is an important point.
Functional programming is a programming style, not a rule that says:
"You are not allowed to write a
forloop."
There are situations where a loop is perfectly reasonable.
The goal is not to make code look "functional."
The goal is to make code:
- Clear
- Predictable
- Reusable
- Maintainable
If a simple loop communicates the idea better, use the loop.
Good programming is about choosing the right tool for the problem.
17. Quick Summary
Here is the easiest way to remember these concepts:
Pure functions
Same input → Same output
No unwanted side effects
Immutability
Don't change existing data unnecessarily.
Create new data instead.
Higher-order functions
Functions can receive functions
or return functions.
map()
Transform every item.
filter()
Keep items that match a condition.
reduce()
Combine many items into one result.
Currying
multiple arguments
↓
series of functions
Example:
add(2)(3);
pipe()
function A → function B → function C
compose()
function C ← function B ← function A
Final Thoughts
The biggest lesson I took from functional programming is that small
functions can be surprisingly powerful when they are combined well.
Instead of building one huge function that knows how to do everything,
we can create small pieces:
filterUsers()
getAdults()
getNames()
sortUsers()
and connect them:
pipe(
filterUsers,
getAdults,
getNames,
sortUsers
);
Each function has a clear job.
That makes the overall program easier to understand because we can look
at the pipeline and almost read it like a sentence.
Functional programming is not about writing clever code.
It is about writing code where the data flow and intention are easy to
see.
And once concepts like pure functions, immutability, higher-order
functions, map, filter, reduce, currying, and composition start to
click, JavaScript becomes a lot more expressive.
Top comments (0)