Callback in javascript
๐ A function that is passed as an argument to another function.
Why do we need callback
โฆ๏ธ JavaScript runs code sequentially in top-down order. However, there are some cases where code runs (or must run) after something else happens and not sequentially. This is called asynchronous programming.
โฆ๏ธ Callbacks help you control the order of execution. It makes sure that a function is not going to run before a task is completed, but will run right after the task has completed. It helps us develop asynchronous JavaScript code and keeps us safe from problems and errors.
How to create a callback function
1. Creating a callback as a function expression
JavaScript
function greetUser(name, callback) {
console.log("Hi " + name);
callback();
}
function sayBye() {
console.log("Bye!");
}
greetUser("Rahul", sayBye);
2. Creating as Anonymous Function (with setTimeout)
JavaScript
setTimeout(function() {
console.log("This message is shown after 3 seconds");
}, 3000);
3. Callback as an Arrow Function
JavaScript
setTimeout(() => {
console.log("This message is shown after 3 seconds");
}, 3000);
All 3 will do the same job.
Callback helps in asynchronous programming
๐ Callback helps in executing code only after a following response.
Here, the function will wait for 3 seconds to print, allowing us to control execution according to our need:
JavaScript
const printMeAfter3sec = () => {
console.log("hello");
}
setTimeout(printMeAfter3sec, 3000);
Let's understand how callback works
JavaScript
console.log("Loading...")
function fetchDataCallback(callback) {
console.log("Fetching data...")
setTimeout(() => {
const data = { id: 1, name: "John Doe" } // Simulating fetched data
callback(data) // Call the callback with the data after fetching
}, 2000) // Simulate a 2-second delay
}
// Using the function with a callback
fetchDataCallback((result) => {
console.log("Data fetched:", result)
})
console.log("End...")
Explanation Step-by-Step
1. Global Execution Context
When JavaScript starts executing this code, it first creates a Global Execution Context. This includes all the variables, function declarations, and any code that needs to run at the top level. The function fetchDataCallback is defined and stored in memory (it isn't executed yet).
2. Function Invocation (Synchronous Part)
JavaScript
fetchDataCallback((result) => {
console.log('Data fetched:', result);
});
fetchDataCallback is invoked with a callback function passed as an argument. A new execution context is created for this function call, where the callback parameter is assigned the anonymous function (result) => { console.log('Data fetched:', result); }.
3. Execution of console.log('Fetching data...')
Inside fetchDataCallback, the first statement is:
JavaScript
console.log('Fetching data...');
This is a synchronous operation, so it immediately logs "Fetching data..." to the console.
4. Encountering setTimeout
Next, we encounter:
JavaScript
setTimeout(() => {
const data = { id: 1, name: 'John Doe' };
callback(data);
}, 2000);
setTimeout is a Web API provided by the browser (or Node.js). It takes two arguments:
- A callback function to execute after the delay.
- A delay in milliseconds (2000ms or 2 seconds).
When JavaScript encounters setTimeout, it offloads this task to the Web API environment.
Important: The Web API handles the timer in the background while the main JavaScript thread continues executing other code.
5. Finishing the Synchronous Code
After setTimeout is sent to the Web API, the synchronous code inside fetchDataCallback finishes execution. At this point, the JavaScript call stack is empty.
6. Timer and Web APIs (Asynchronous Part)
While the main thread is free, the Web API timer runs in the background for 2 seconds (2000ms). After 2 seconds, the timer expires, and the callback function inside setTimeout is sent to the task queue.
7. Event Loop
The event loop continuously monitors the call stack and the task queue. Once the call stack is empty, the event loop pushes tasks from the task queue to the call stack.
8. Callback Execution (Async Code)
After 2 seconds, the event loop pushes the callback function inside setTimeout to the call stack:
JavaScript
() => {
const data = { id: 1, name: 'John Doe' };
callback(data);
}
Inside this function:
- The data object is created: { id: 1, name: 'John Doe' }.
- The callback function is invoked with this data object as an argument.
9. Executing the Passed Callback
Now, (result) => { console.log('Data fetched:', result); } is called with data as result.
A new execution context is created for this callback, logging:
Data fetched: { id: 1, name: 'John Doe' }.
Visual Timeline
- Start: Function fetchDataCallback is called โ logs "Fetching data...".
- setTimeout: Offloaded to Web APIs โ 2-second timer starts.
- Synchronous Code Ends: Main thread becomes idle โ waits for async tasks.
- After 2 Seconds: Timer expires โ callback is pushed to task queue โ event loop moves it to the call stack.
- Callback Executes: Logs the final data.
Summary of Web APIs Role
The Web API (setTimeout) is responsible for the timer. The event loop ensures that after the timer expires, the callback is executed asynchronously once the call stack is clear. Callbacks allow us to handle asynchronous operations where some tasks (like waiting for data) are handled outside of the JavaScript execution engine.
Problem Statement: Fix with Callback
In the code below, the output of message returns undefined:
JavaScript
console.log("hello");
function asyncMe(name) {
setTimeout(function () {
return `${name}`;
}, 1000);
}
const message = asyncMe("himanshu");
console.log(message);
console.log("bye");
Reason: The message output is undefined because asyncMe() is an asynchronous function, meaning the timer callback executes later, after the synchronous return has already resolved to undefined.
Solution:
Define a function inside a parameter (i.e., callback) and pass it inside setTimeout():
JavaScript
console.log("hello");
function asyncMe(name, callback) {
setTimeout(function () {
const message = `print to ${name}`;
callback(message); // Call the callback function with the message
}, 1000);
}
asyncMe("himanshu", function(message) {
console.log(message); // Executed after the timeout
});
console.log("bye");
Question: Fix this with callback
Problem Code:
JavaScript
function getUserData(id) {
setTimeout(() => {
if (!id) {
throw new Error("User ID is required!");
}
return { id, name: "John Doe" }; // This return won't work properly in async code
}, 1000);
}
const user = getUserData(1);
console.log(user); // This will be undefined
Solution:
JavaScript
function getUserData(id, callback) {
setTimeout(() => {
if (!id) {
return callback(new Error("User ID is required!"), null);
}
const user = { id, name: "John Doe" };
callback(null, user);
}, 1000);
}
// Using the function with a callback
getUserData(1, (error, user) => {
if (error) {
console.error("Error:", error.message);
} else {
console.log("User Data:", user);
}
});
// Testing the error case
getUserData(null, (error, user) => {
if (error) {
console.error("Error:", error.message);
} else {
console.log("User Data:", user);
}
});
Other uses of callback
1. Callback helps in calling an event
HTML
<!-- HTML -->
<button id="callback-btn">Click here</button>
JavaScript
// JavaScript
document.querySelector("#callback-btn")
.addEventListener("click", function() {
console.log("User has clicked on the button!");
});
The first parameter is its type ("click"), and the second parameter is a callback function which logs the message when the button is clicked.
2. Callback function helps in creating a polyfill
For example, creating a polyfill for forEach:
JavaScript
let arr = [2, 4, 5, 3, 5];
const myForEach = (arr, cb) => {
for (let i = 0; i < arr.length; i++) {
const element = arr[i];
cb(element);
}
};
myForEach(arr, (name) => console.log(name));
๐ Homework: Try creating polyfills for map, filter, and reduce.