DEV Community

koushikmaya
koushikmaya

Posted on

# Functional Programming in JavaScript: Writing Code That Is Easier to Understand

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(), and reduce()
  • Currying
  • Composition with pipe() and compose()

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]
Enter fullscreen mode Exit fullscreen mode

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:

  1. The same input always produces the same output.
  2. 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
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

This is easy to understand:

square(4); // 16
square(10); // 100
Enter fullscreen mode Exit fullscreen mode

We know exactly what the function will return.


Impure functions

Now consider this:

let total = 10;

function addToTotal(value) {
    total += value;
    return total;
}
Enter fullscreen mode Exit fullscreen mode

This function changes the external variable total.

So the result depends on what happened before:

addToTotal(5); // 15
addToTotal(5); // 20
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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];
Enter fullscreen mode Exit fullscreen mode

We could modify it directly:

numbers.push(4);
Enter fullscreen mode Exit fullscreen mode

Now the original array has changed:

console.log(numbers);
// [1, 2, 3, 4]
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The interesting part is:

multiplyBy(2)
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

Think of map() as:

one item → transform it → one new item
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

The callback returns either:

true
Enter fullscreen mode Exit fullscreen mode

or

false
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The second argument:

0
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

The result becomes:

{
    developer: ["Asha", "Maya"],
    designer: ["Rahul"]
}
Enter fullscreen mode Exit fullscreen mode

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];
Enter fullscreen mode Exit fullscreen mode

map()

numbers.map(n => n * 2);
Enter fullscreen mode Exit fullscreen mode

Result:

[2, 4, 6, 8, 10]
Enter fullscreen mode Exit fullscreen mode

filter()

numbers.filter(n => n > 2);
Enter fullscreen mode Exit fullscreen mode

Result:

[3, 4, 5]
Enter fullscreen mode Exit fullscreen mode

reduce()

numbers.reduce((sum, n) => sum + n, 0);
Enter fullscreen mode Exit fullscreen mode

Result:

15
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

we can write:

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

console.log(add(2)(3));
// 5
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

It may look strange initially, but break it down:

add(2)
Enter fullscreen mode Exit fullscreen mode

returns:

b => 2 + b
Enter fullscreen mode Exit fullscreen mode

Then:

add(2)(3)
Enter fullscreen mode Exit fullscreen mode

becomes:

2 + 3
Enter fullscreen mode Exit fullscreen mode

which gives:

5
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

We can manually combine them:

const result = addTen(double(5));

console.log(result);
// 20
Enter fullscreen mode Exit fullscreen mode

The flow is:

5
 ↓
double
 ↓
10
 ↓
addTen
 ↓
20
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

Now we can write:

const double = number => number * 2;

const addTen = number => number + 10;

const processNumber = pipe(
    double,
    addTen
);

console.log(processNumber(5));
// 20
Enter fullscreen mode Exit fullscreen mode

The data flows like this:

5
 ↓
double
 ↓
10
 ↓
addTen
 ↓
20
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

Now:

const double = number => number * 2;

const addTen = number => number + 10;

const processNumber = compose(
    addTen,
    double
);

console.log(processNumber(5));
// 20
Enter fullscreen mode Exit fullscreen mode

The execution is:

5
 ↓
double
 ↓
10
 ↓
addTen
 ↓
20
Enter fullscreen mode Exit fullscreen mode

The important difference is how the functions are listed.

pipe()

pipe(double, addTen);
Enter fullscreen mode Exit fullscreen mode

Reads:

double → addTen
Enter fullscreen mode Exit fullscreen mode

compose()

compose(addTen, double);
Enter fullscreen mode Exit fullscreen mode

Reads from right to left:

double → addTen
Enter fullscreen mode Exit fullscreen mode

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 }
];
Enter fullscreen mode Exit fullscreen mode

Our goal is:

  1. Keep active users.
  2. Keep users who are adults.
  3. 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);
Enter fullscreen mode Exit fullscreen mode

Now we can compose them:

const getActiveAdultNames = pipe(
    getActiveUsers,
    getAdults,
    getNames
);

console.log(getActiveAdultNames(users));
Enter fullscreen mode Exit fullscreen mode

Result:

["Asha", "Arjun"]
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

keep the required data as input when possible.


2. Do I really need to mutate this data?

Instead of:

items.push(newItem);
Enter fullscreen mode Exit fullscreen mode

consider:

const updatedItems = [...items, newItem];
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 for loop."

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
Enter fullscreen mode Exit fullscreen mode

Immutability

Don't change existing data unnecessarily.
Create new data instead.
Enter fullscreen mode Exit fullscreen mode

Higher-order functions

Functions can receive functions
or return functions.
Enter fullscreen mode Exit fullscreen mode

map()

Transform every item.
Enter fullscreen mode Exit fullscreen mode

filter()

Keep items that match a condition.
Enter fullscreen mode Exit fullscreen mode

reduce()

Combine many items into one result.
Enter fullscreen mode Exit fullscreen mode

Currying

multiple arguments
        ↓
series of functions
Enter fullscreen mode Exit fullscreen mode

Example:

add(2)(3);
Enter fullscreen mode Exit fullscreen mode

pipe()

function A → function B → function C
Enter fullscreen mode Exit fullscreen mode

compose()

function C ← function B ← function A
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

and connect them:

pipe(
    filterUsers,
    getAdults,
    getNames,
    sortUsers
);
Enter fullscreen mode Exit fullscreen mode

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)