From Loops to Functions: My Journey to Writing Cleaner JavaScript


Introduction
We all use JavaScript when developing applications — whether on the front-end or back-end. But the real question is: are we writing clean, maintainable code?
So, what exactly is "clean code"? Clean code is code that's easy to understand, reduces redundancy, and minimizes tight coupling between components. It’s efficient, readable, and less prone to bugs or exceptions.
To write clean code, developers often follow specific paradigms — the most common being Object-Oriented Programming (Oops) and Functional Programming.
JavaScript stands out because it supports both approaches and offers a dynamic and flexible style of writing code. In particular, higher-order functions play a crucial role in the functional programming world — helping us write less repetitive, more modular, and error-resistant code.
In this article, we’ll explore what higher-order functions are, how they help us write cleaner code, and how you can start using them effectively in your JavaScript projects.
What are higher order functions in JavaScript ?
In JavaScript, higher-order functions are those that receive a function as an input, return a function as output, or both — enabling more flexible and reusable code.
Some commonly used higher-order functions in JavaScript include map()
, filter()
, reduce()
, and callback functions. These functions either take other functions as arguments or return them, making your code more modular and expressive.
To make the above definition easier to understand, let's look at an example of a higher-order function named A.
function A() {
return function B() {
console.log("Hello, I am function B");
};
}
const x = A(); // A returns function B
x(); // Executes B => Logs: Hello, I am function B
Explanation:
A is a higher-order function because it returns another function (B).
When you call A(), it returns B, and you store it in X.
Then X() executes B, printing the message.
Common built-in higher order functions
map()
The map()
function creates a new array by applying a provided function to each element of the original array.
First, let’s understand how the map()
function works internally, and then we'll explore how to use it effectively in real-world scenarios.
// internal working of map
function customMap(arr,callback){
const result = [];
for(i=0;i<arr.length;i++){
const updatedValue = callback(arr[i],i,arr);
result.push(updatedValue);
}
return result;
}
const array = [1,2,3,4];
// Using customMap (mimics map)
const doubledCustom = customMap(array,(num) => num + num);// we are passing a callback function
console.log(doubledCustom); // [2,4,6,8]
// Using built-in map to achieve the same result
cosnt doubled = array.map((num) => num + num);
console.log(doubled); //[2,4,6,8]
- filter()
The filter()
function creates a new array by including only those elements from the original array that satisfy a given condition provided by the callback function.
First, let’s understand how the filter()
function works internally, and then we'll explore how to use it effectively in real-world scenarios.
// Internal working of filter
function customFilter(arr, callback) {
const result = [];
for (let i = 0; i < arr.length; i++) {
if (callback(arr[i], i, arr)) {
result.push(arr[i]); // Only push if condition is true
}
}
return result;
}
const array = [1, 2, 3, 4, 5];
// Using customFilter to get even numbers
const evenNumbersCustom = customFilter(array, (num) => num % 2 === 0);
console.log(evenNumbersCustom); // [2, 4]
// Using built-in filter to get even numbers
const evenNumbers = array.filter((num) => num % 2 === 0);
console.log(evenNumbers); // [2, 4]
- reduce()
The reduce()
function processes each element of an array and reduces it to a single accumulated value using a callback function and an optional initial value.
First, let’s understand how the reduce()
function works internally, and then we'll explore how to use it effectively in real-world scenarios.
// Internal working of reduce
function customReduce(arr, callback, initialValue) {
let accumulator = initialValue !== undefined ? initialValue : arr[0];
let startIndex = initialValue !== undefined ? 0 : 1;
for (let i = startIndex; i < arr.length; i++) {
accumulator = callback(accumulator, arr[i], i, arr);
}
return accumulator;
}
const array = [1, 2, 3, 4];
// Using customReduce to calculate sum
const sumCustom = customReduce(array, (acc, curr) => acc + curr, 0);
console.log(sumCustom); // Output: 10
// Using built-in reduce to calculate sum
const sum = array.reduce((acc, curr) => acc + curr, 0);
console.log(sum); // Output: 10
Why Use Higher-Order Functions?
Promote Code Re usability:
Instead of repeating logic in multiple places, you can create a general-purpose function that accepts a behavior (callback) and reuses it with different data.
function repeat(n, callback) {
for (let i = 0; i < n; i++) {
callback(i);
}
}
repeat(3, console.log); // Reuses logic for different operations
Make Code More Declarative :
HOFs like map()
, filter()
, and reduce()
make your code easier to read and understand by expressing what should happen rather than how it should happen.
// Imperative
let result = [];
for (let i = 0; i < nums.length; i++) {
if (nums[i] > 10) result.push(nums[i]);
}
// Declarative
const result = nums.filter(num => num > 10);
Enable Composition and Abstraction :
You can pass logic around like data — this lets you compose complex behaviors from smaller ones.
function greet(name) {
return `HELLO, ${name}`;
}
function upperCaseDecorator(fn) {
return function (name) {
return fn(name).toUpperCase();
};
}
const loudGreet = upperCaseDecorator(greet);
console.log(loudGreet("JHON")); // HELLO, JHON
Work Well with Asynchronous Code :
Functions like setTimeout, fetch, or Promises often rely on callbacks — which are a form of higher-order function usage.
setTimeout(() => {
console.log("This runs after 1 second");
}, 1000);
Writing Your Own Higher-Order Function
So far, we've seen how built-in functions like map()
, filter()
, and reduce()
are higher-order functions. But did you know you can write your own higher-order functions too?
A higher-order function is simply any function that:
Accepts another function as an argument, or
Returns a function
Let’s build a few simple examples to understand this better:
function executeTwice(callback) {
callback();
callback();
}
function sayHello() {
console.log("Hello!");
}
executeTwice(sayHello);
// Output:
// Hello!
// Hello!
Here, executeTwice is a higher-order function because it takes another function (sayHello) as an argument and calls it twice.
Here’s another basic custom higher-order function that takes a callback:
function createValidator(validationFn) {
return function (input) {
return validationFn(input);
};
}
const isNumber = createValidator((value) => typeof value === "number");
console.log(isNumber(10)); // true
console.log(isNumber("10")); // false
Best Practices When Using Higher-Order Functions
Higher-order functions (HOFs) are powerful tools in JavaScript, but like any tool, they’re most effective when used thoughtfully. Here are some best practices to follow when working with them:
Use Descriptive Callback Function Names
Instead of using anonymous functions everywhere, give meaningful names when the logic is complex or reused.
function isEven(num) {
return num % 2 === 0;
}
const evenNumbers = [1, 2, 3, 4].filter(isEven);
Keep Callbacks Pure
A pure function has no side effects. It always returns the same output for the same input. Keeping your callbacks pure makes code easier to test and debug.
// Pure
const squared = numbers.map(n => n * n);
// Not pure (has side effects)
numbers.map(n => {
externalVar += n;
return n;
});
Avoid Over-Nesting
Chaining too many HOFs like map() .filter() .reduce()
can reduce readability. Break the logic into smaller steps when necessary.
// Okay
const result = arr.map(x => x * 2).filter(x => x > 10);
// Better if it’s long
const doubled = arr.map(x => x * 2);
const result = doubled.filter(x => x > 10);
Don’t Mutate the Original Array
HOFs like map()
, filter()
, and reduce()
return new arrays — avoid modifying the original array inside the callback.
// Avoid this
arr.map((item, i) => arr[i] = item * 2);
// Do this
const doubled = arr.map(item => item * 2);
- Use Arrow Functions for Simplicity
Use arrow functions when the logic is short and simple — it makes code cleaner.
// Clean and concise
const squared = arr.map(n => n * n);
When not to use HOFs
You Need to Break or Continue Early
HOFs don't support break or continue. If you need to exit the loop midway, a for or while loop is better.
for (let i = 0; i < arr.length; i++) {
if (arr[i] > 10) break; // Can't do this in map() or forEach()
}
You’re Working with Asynchronous Logic
forEach() doesn’t wait for async/await to complete. Use for…of instead for sequential async operations.
// Won't work as expected
arr.forEach(async (item) => {
await doSomething(item);
});
// Correct
for (const item of arr) {
await doSomething(item);
}
Performance is Critical
In performance-sensitive areas (like large loops or games), HOFs may introduce slight overhead due to function calls. Traditional loops are more performant in such cases.
You’re Writing Complex Logic
Chaining multiple HOFs .map() .filter() .reduce() can become hard to read or debug. Break them into separate steps or use loops for clarity.
You Intend to Mutate the Original Array
HOFs are designed to be pure and return new arrays. If you need to mutate the original array intentionally, use loops carefully.
// Not recommended
arr.map((item, i) => arr[i] = item * 2);
// Use loop instead
for (let i = 0; i < arr.length; i++) {
arr[i] = arr[i] * 2;
}
Conclusion + Key Takeaways
Higher-order functions (HOFs) are a cornerstone of modern JavaScript development. They allow you to write more readable, reusable, and expressive code by treating functions as first-class citizens — passing them around just like variables.
Whether you’re transforming data with map(), filtering values with filter(), or aggregating results using reduce(), HOFs help you focus on what you want to do, not just how to do it.
They’re not just about syntax — they encourage a mindset shift toward functional programming, enabling you to write code that is easier to test, debug, and extend.
key Takeaways
Higher-order functions are functions that either accept another function as an argument, return a function, or do both.
Common HOFs in JavaScript include: map(), filter(), reduce(), and custom callbacks.
HOFs help you follow DRY (Don't Repeat Yourself) principles by making your code modular and reusable.
You can write your own higher-order functions to abstract repetitive patterns or inject dynamic behavior.
Avoid using HOFs in cases requiring early loop exits, in-place mutations, or tight async control.
HOFs promote a functional and declarative style — focusing on what should be done rather than how.
Let’s Connect
💡 Follow me here on hashnode for more JavaScript insights
🧵 Share this blog with someone who's learning JavaScript!
Subscribe to my newsletter
Read articles from Rishabh Mishra directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
