Callbacks and Closures in JavaScript: A Clear Comparison

IndrajeetIndrajeet
7 min read

In JavaScript, callback functions and closures are two essential concepts that involve functions within functions. While both allow for greater flexibility and control over code execution, they serve different purposes:

  • Callbacks are functions passed as arguments to other functions and executed later, often used for asynchronous operations like API calls, event handling, and sequencing tasks.

  • Closures are functions that remember the scope in which they were created, allowing them to maintain private variables and state even after their parent function has executed.

Understanding these concepts is crucial for writing efficient, reusable, and modular JavaScript code. Let’s understand one by one.

πŸ”Ή Callback Functions

A callback function is a function that is passed as an argument to another function and is executed later, usually after some asynchronous operation is completed.

βœ… Purpose:

  • Used to handle asynchronous operations (e.g., setTimeout, event listeners, API calls).

  • Helps in executing functions in sequence (important in async programming).

πŸ”₯ Example :

function fetchData(callback) {
    setTimeout(() => {
        console.log("Data fetched");
        callback();  // Executing the callback function
    }, 2000);
}

function processData() {
    console.log("Processing data...");
}

fetchData(processData);  // Passing processData as a callback

πŸ“Œ Here: processData is passed as a callback to fetchData, ensuring it runs after the data is fetched.


πŸ”Ή More Examples of Callback Functions

πŸ›  1. Using Callbacks in an API Request (Simulated)

function fetchUserData(userId, callback) {
    console.log(`Fetching data for user ${userId}...`);
    setTimeout(() => {
        const user = { id: userId, name: "Joe" };
        callback(user); // Calling the callback after fetching data
    }, 2000);
}

function displayUser(user) {
    console.log(`User Name: ${user.name}`);
}

fetchUserData(101, displayUser);

πŸ“Œ Here:

  • fetchUserData simulates fetching user data from an API.

  • displayUser is passed as a callback and runs after fetching data.


πŸ— 2. Callbacks in Event Handling

document.getElementById("myButton").addEventListener("click", function() {
    console.log("Button was clicked!");
});

πŸ“Œ Here:

  • The function inside addEventListener is a callback function, executed when the button is clicked.

πŸš€ 3. Callbacks in Array Methods

const numbers = [1, 2, 3, 4, 5];

const doubled = numbers.map(function(num) {
    return num * 2;
});

console.log(doubled); // [2, 4, 6, 8, 10]

πŸ“Œ Here:

  • map takes a callback function to process each element in the array.

πŸ•’ 4. Simulating an API Call with Callbacks

function fetchData(url, successCallback, errorCallback) {
    console.log(`Fetching data from ${url}...`);

    setTimeout(() => {
        let success = Math.random() > 0.3; // Simulating 70% success rate

        if (success) {
            successCallback({ data: "Sample API Data" });
        } else {
            errorCallback("Error: Unable to fetch data!");
        }
    }, 2000);
}

fetchData(
    "https://api.example.com/data",
    (response) => console.log("Data received:", response.data),
    (error) => console.error(error)
);

πŸ“Œ Here:

  • If the API call is successful, successCallback is executed.

  • Otherwise, errorCallback handles the failure.


πŸ”„ 5. Using Callbacks to Control Execution Order

function step1(callback) {
    console.log("Step 1 completed.");
    callback();
}

function step2(callback) {
    console.log("Step 2 completed.");
    callback();
}

function step3() {
    console.log("Step 3 completed.");
}

step1(() => step2(() => step3()));

πŸ“Œ Here:

  • The functions execute in order because each function calls the next one.

πŸ† 6. Using Callbacks in Sorting

const students = [
    { name: "Alice", age: 22 },
    { name: "Bob", age: 25 },
    { name: "Charlie", age: 20 }
];

students.sort((a, b) => a.age - b.age); // Sort by age (ascending)

console.log(students);

πŸ“Œ Here:

  • sort takes a callback to compare student ages.

🎡 7. Using Callbacks for Audio Player

function playSong(song, callback) {
    console.log(`Playing ${song}...`);
    setTimeout(callback, 3000); // Simulate song duration
}

function nextSong() {
    console.log("Playing next song...");
}

playSong("Song 1", nextSong);

πŸ“Œ Here:

  • nextSong runs after the current song finishes.

πŸ”Ή Closures

A closure is a function that "remembers" the scope in which it was created, even after that scope has finished executing.

βœ… Purpose:

  • Preserves variable state even after the outer function has returned.

  • Useful for data encapsulation (e.g., private variables).

  • Avoids global scope pollution.

πŸ”₯ Example:

function createCounter() {
    let count = 0;  // Private variable

    return function() {
        count++;
        console.log(`Count: ${count}`);
    };
}

const counter = createCounter();  // Returns inner function
counter(); // Count: 1
counter(); // Count: 2
counter(); // Count: 3

πŸ“Œ Here:

  • The inner function "remembers" count even after createCounter is finished.

  • This is because the function closes over (captures) the count variable.

πŸ”Ή More Examples of Closures

🎯 1. Creating a Private Counter

function createCounter() {
    let count = 0;

    return function() {
        count++; // "count" is remembered
        console.log(`Current count: ${count}`);
    };
}

const counter = createCounter();

counter(); // Current count: 1
counter(); // Current count: 2
counter(); // Current count: 3

πŸ“Œ Here:

  • The inner function retains access to count even though createCounter has already executed.

🏦 2. Closure for Creating a Bank Account

function createBankAccount(initialBalance) {
    let balance = initialBalance;

    return {
        deposit: function(amount) {
            balance += amount;
            console.log(`Deposited: $${amount}, New Balance: $${balance}`);
        },
        withdraw: function(amount) {
            if (amount > balance) {
                console.log("Insufficient funds!");
            } else {
                balance -= amount;
                console.log(`Withdrew: $${amount}, New Balance: $${balance}`);
            }
        },
        checkBalance: function() {
            console.log(`Balance: $${balance}`);
        }
    };
}

const myAccount = createBankAccount(1000);

myAccount.deposit(500);    // Deposited: $500, New Balance: $1500
myAccount.withdraw(200);   // Withdrew: $200, New Balance: $1300
myAccount.checkBalance();  // Balance: $1300

πŸ“Œ Here:

  • balance is private inside createBankAccount, but accessible through the returned functions.

πŸ”₯ 3. Function Factory (Custom Multipliers)

function createMultiplier(multiplier) {
    return function(num) {
        return num * multiplier;
    };
}

const double = createMultiplier(2);
const triple = createMultiplier(3);

console.log(double(5));  // 10
console.log(triple(5));  // 15

πŸ“Œ Here:

  • The inner function "remembers" multiplier, even after createMultiplier has finished executing.

🏦 4. Creating a Secure Wallet

function createWallet(initialBalance) {
    let balance = initialBalance;

    return {
        deposit(amount) {
            balance += amount;
            console.log(`Deposited $${amount}. New Balance: $${balance}`);
        },
        withdraw(amount) {
            if (amount > balance) {
                console.log("Insufficient funds!");
            } else {
                balance -= amount;
                console.log(`Withdrew $${amount}. New Balance: $${balance}`);
            }
        },
        getBalance() {
            return balance;
        }
    };
}

const myWallet = createWallet(500);
myWallet.deposit(200); // Deposited $200. New Balance: $700
myWallet.withdraw(100); // Withdrew $100. New Balance: $600
console.log(myWallet.getBalance()); // 600

πŸ“Œ Here:

  • balance is private and can only be modified via deposit and withdraw.

πŸ“’ 5. Custom Event Logger

function createLogger(module) {
    return function(message) {
        console.log(`[${module}] ${message}`);
    };
}

const authLogger = createLogger("Auth");
const dbLogger = createLogger("Database");

authLogger("User logged in."); // [Auth] User logged in.
dbLogger("Connection established."); // [Database] Connection established.

πŸ“Œ Here:

  • createLogger returns a function that "remembers" the module name.

πŸ•΅οΈ 6. Private Counter

function counter() {
    let count = 0;

    return {
        increment() {
            count++;
            console.log(`Count: ${count}`);
        },
        decrement() {
            count--;
            console.log(`Count: ${count}`);
        }
    };
}

const myCounter = counter();
myCounter.increment(); // Count: 1
myCounter.increment(); // Count: 2
myCounter.decrement(); // Count: 1

πŸ“Œ Here:

  • count remains private, preventing direct modification.

πŸ”₯ 7. Function that Returns Different Greetings

function greeting(language) {
    return function(name) {
        if (language === "en") return `Hello, ${name}!`;
        if (language === "es") return `Hola, ${name}!`;
        if (language === "fr") return `Bonjour, ${name}!`;
        return `Hi, ${name}!`;
    };
}

const englishGreet = greeting("en");
const spanishGreet = greeting("es");

console.log(englishGreet("Joe")); // Hello, Joe!
console.log(spanishGreet("Carlos")); // Hola, Carlos!

πŸ“Œ Here:

  • greeting("en") creates a closure that remembers "en".

🎭 8. Delayed Execution with Closures

function delayedMessage(message, delay) {
    return function() {
        setTimeout(() => {
            console.log(message);
        }, delay);
    };
}

const greetLater = delayedMessage("Hello, after 3 seconds!", 3000);
greetLater();

πŸ“Œ Here:

  • The inner function remembers message and delay even after delayedMessage executes.

🎰 9. Lottery Number Generator

function lottery() {
    let secretNumber = Math.floor(Math.random() * 100) + 1;

    return function(guess) {
        if (guess === secretNumber) {
            console.log("Congratulations! You guessed the right number.");
        } else {
            console.log("Try again!");
        }
    };
}

const play = lottery();
play(50); // "Try again!"
play(75); // "Try again!"
play(30); // "Congratulations!" (if correct)

πŸ“Œ Here:

  • secretNumber remains private, ensuring fair play.

πŸ”₯ Key Differences

FeatureCallback FunctionsClosures
DefinitionFunction passed as an argument and executed laterFunction that remembers the scope it was created in
PurposeHandles async operations or executes functions in sequenceRetains variables even after the outer function has finished
ExecutionCalled later inside another functionExecutes immediately when invoked
Example Use CasessetTimeout, API calls, event listenersData encapsulation, state management

🎯 Final Thoughts

  • Use Callbacks when handling async operations, executing functions after an event, or processing data sequentially.

  • Use Closures when you need private variables, persistent state, or function customization.

Conclusion :

Both callback functions and closures enable more dynamic, flexible, and functional programming in JavaScript. Callbacks help manage async operations and execution flow, while closures allow functions to retain state and create encapsulated logic.

By mastering these concepts, you can write more efficient, maintainable, and scalable JavaScript applications, whether you’re working with event-driven programming, API calls, or modular design patterns. πŸš€

0
Subscribe to my newsletter

Read articles from Indrajeet directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Indrajeet
Indrajeet

Hello there! My name is Indrajeet, and I am a skilled web developer passionate about creating innovative, user-friendly, and dynamic web applications. πŸ“ŒMy Expertise advanced proficiency in angular front-End Development. Back-End Development Leveraging the power of .NET, I build secure, robust, and scalable server-side architectures.