Mastering JavaScript: Unraveling the Power of Web Development
JavaScript is an asynchronous language, which means that it can handle multiple tasks simultaneously. Callbacks are a fundamental aspect of JavaScript, as they allow you to run code after an asynchronous operation has been completed. In this article, we’ll look at what callbacks are, why they’re used, and how to use them with real-life examples and code examples.
What are Callbacks?
A callback is a function that is passed as an argument to another function, and is called after the main function has finished its execution. The main function is called with a callback function as its argument, and when the main function is finished, it calls the callback function to provide a result. Callbacks allow you to handle the results of an asynchronous operation in a non-blocking manner, which means that the program can continue to run while the operation is being executed.
Why use Callbacks?
Javascript is a single-threaded language, which means, it executes the code sequentially one line after another. However, there are some cases in which a part of the code will only run after some lines of code, i.e. it is dependent on another function. This is called asynchronous programming.
We need callback functions to make sure that a function is only going to get executed after a specific task is completed and not before its completion.
- For instance, in the case of an event listener, we first listen for a specific event, and if the function detects it, then only the callback function is executed. It's just like when you go into a restaurant, the function(action) to make a pizza will only run after you order a pizza. And in the meantime, the restaurant staff(browser or nodeJS) will do some other work.
Callback functions are also needed to prevent running into errors because of the non-availability of some data that are needed by some other function.
- For example, when a function fetches some data from an API that takes some time to get downloaded. We use the callback function here so that the function that needs the data will only get executed when all the required data is downloaded.
Examples
Let's see a few examples of callback functions and how it's used in javascript :
1. Synchronous Callback Function
The code executes sequentially - synchronous programming.
console.log('Start')
function divide(a, b) {
console.log(a / b)
}
function operation(val1, val2, callback) {
callback(val1, val2)
}
operation(16, 4, divide)
console.log('End')
Output :
Start
4
End
2. setTimeout() - Asynchronous Callback Function
console.log('Start')
setTimeout(() => {
console.log('We are in the setTimeout()')
}, 4000)
console.log('End')
Output :
Start
End
We are in the setTimeout()
The execution of this code is different than what we are used to(synchronous).
Let's see it in detail :
Here, as we can see the code is not running sequentially. The console.log("End"); statement get executed before the setTimeout() function. This is called asynchronous programming.
setTimeout() function takes two parameters the first one is a callback function and the second parameter is the time value in milliseconds. The callback function will get executed after the amount of time specified in the second parameter.
The browser or the nodeJS engine will not wait for the setTimeout() function to execute its callback function. In the meantime, it'll move to the next statement and execute that, in our case, that is console.log("End");.
Let's see the execution of the asynchronous program in detail :
The browser or the nodeJS has a few components that help in executing the javascript and rendering a web page.
It consists of a call stack, web APIs, event loop, and callback queue.
The execution of each function happens inside the call stack.
The web APIs consist of features that make javascript a superpower. It has setTimeout(), setInterval(), DOM, and other functions and APIs that allows javascript to do a whole bunch of useful things.
In the above code when we use the setTimeout() function the javascript engine uses the web APIs and gives access to the setTimeout() function.
This function does not go inside the call stack and blocks the execution. The function gets processed in the web APIs section and when the specified time is over the callback function moves to the callback queue.
So the call stack is free to execute the other code while the web API is getting processed and the callback function waits in the callback queue.
When the event loop finds the call stack empty and sees that there is a callback function in the callback queue. The event loop moves that callback function in the call stack and it gets executed.
3. Asynchronous Callback Function
When we fetch data from a server we get it after some delay. The browser doesn't stay idle for that time and it moves to execute the next statement in the javascript program. To execute a block of code or provide the fetched data to the next function in the program we use the asynchronous callback function.
Note :
Here we are using the setTimeout() function to imitate the delay in sending the data by the server.
console.log('Start')
function loginUserServer(email, callback) {
setTimeout(() => {
console.log('We have the data')
callback({ userEmail: email })
}, 5000)
}
const user = loginUserServer('pabhinaw@gmail.com', (user) => {
console.log(user)
})
console.log('End')
Output :
Start
End
We have the data
{ userEmail: 'pabhinaw@gmail.com' }
Here,
If we will not use a callback function we will not get the userEmail from the loginUserServer.
- It will give undefined in place of userEmail because at the time of calling the loginUserServer the data from the server is not available(as it takes 5seconds) this is called hoisting.
However, because we are using the callback function in the loginUserServer() function, the callback waits for the parent function to finish, and then only it executes itself.
Nesting Callbacks and Callback Hell
We can nest callback functions as we nest for loops or if-statements. When we have to run a piece of code that is dependent on some other function and that function is also dependent on the result of another function, the nesting of callback functions happens. This way we can run multiple functions that are dependent on some other functions.
See the program below to get more clarity.
console.log('Start')
function loginUserServer(email, callback) {
setTimeout(() => {
console.log('We have the data')
callback({ userEmail: email })
}, 3000)
}
function getUserID(email, callback) {
setTimeout(() => {
callback(['23', '13', '28', '2'])
}, 2000)
}
const user = loginUserServer('pabhinaw@gmail.com', (userInfo) => {
console.log(userInfo) // First Callback
getUserID(userInfo.userEmail, (userIDs) => {
console.log(userIDs) // Second Callback
})
})
console.log('End')
In the above example, we can see one callback nested into another callback function.
The above code looks messed up and it will get even messier with the increase in the number of nested callbacks. This situation is called callback hell where the code is messed up due to multiple nested callback functions. This decreases the readability of the code and makes it difficult to edit.
So to avoid this issue we use different techniques like promises, async, and await(syntax coating).
Example :
console.log('Start')
function loginUserServer(email, callback) {
setTimeout(() => {
console.log('We have the data')
callback({ userEmail: email })
}, 3000)
}
function getUserID(email, callback) {
setTimeout(() => {
callback(['23', '13', '28', '2'])
}, 2000)
}
function getUserIDname(ID, callback) {
setTimeout(() => {
callback(`ID ${ID} name`)
}, 2000)
}
// Callback Hell
const user = loginUserServer('pabhinaw@gmail.com', (userInfo) => {
console.log(userInfo)
getUserID(userInfo.userEmail, (userIDs) => {
console.log(userIDs)
getUserIDname(userIDs[2], (IDname) => {
console.log(IDname)
})
})
})
console.log('End')
As you can see the above code does look like hell! There are some ways to prevent callback hell. Let's see them in detail.
a. Using Promises
A Promise is a javascript object that links producing code and consuming code. Producing code is the code that takes some time like getting data from a server. Consuming code is the code that must wait for the result of the producing code.
Refactoring the above code using the promise :
console.log('Start')
function loginUserServer(email) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('We have the data')
resolve({ userEmail: email })
}, 3000)
})
}
function getUserID(email) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(['23', '13', '28', '2'])
}, 2000)
})
}
function getUserIDname(ID) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`ID ${ID} name`)
}, 2000)
})
}
loginUserServer('pabhinaw@gmail.com')
.then((userInfo) => getUserID(userInfo.email))
.then((userID) => getUserIDname(userID[2]))
.then((userIDname) => console.log(userIDname))
console.log('End')
Output :
Start
End
We have the data
ID 28 name
This looks much cleaner, and easy to read, and edit. This is one of the advantages of using promises in javascript.
Let's see javascript promises in a brief :
A promise has four states :
Pending
Settled
Fulfilled
Rejected
Using these different states a promise does efficient error handling.
When the action related to the promise is neither fulfilled nor rejected, it's in a pending state.
When the action related to the promise is either fulfilled or rejected, it's in a settled state.
When the action related to the promise is executed successfully(resolved), it's in the fulfilled state.
When the action related to the promise fails(not resolved) due to some error, it's in the rejected state.
Using the .then() method we can do further actions like calling another callback function or catching the error.
b. Using Async and Await
Using javascript promise we can prevent callback hell and can catch the errors and respond to them. However, the way of writing promise code is something we are not used to(it's the asynchronous way). Using async and await we can write the asynchronous javascript promises code synchronously. In the background it's still using the whole javascript promise code, this is just a better syntax, easier to understand, and write.
console.log('Start')
function loginUserServer(email) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('We have the data')
resolve({ userEmail: email })
}, 3000)
})
}
function getUserID(email) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(['23', '13', '28', '2'])
}, 2000)
})
}
function getUserIDname(ID) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`ID ${ID} name`)
}, 2000)
})
}
async function displayUser() {
const loggedUser = await loginUserServer('pabhinaw@gmail.com')
const loggedUserID = await getUserID(loggedUser.userEmail)
const loggedUserIDname = await getUserIDname(loggedUserID[2])
console.log(loggedUserIDname)
}
displayUser()
console.log('End')
Output :
Start
End
We have the data
ID 28 name
In the above code, we use the syntax coating of the async and await.
We create a function and use the async keyword to tell the browser that we are going write an asynchronous function.
After declaring the function async only we can use await.
We use the await keyword to wait for the callback function to get executed and then only assign value to the local variables.
Conclusion
The Callback function in javascript is an important concept and it's used everywhere like :
In setTimeout(), setInterval(), and addEventListener() functions.
These functions will be very frequently used in your projects from beginner level to even advanced ones.
Callback functions start nesting when the size and the complexity of the projects increase and this might cause callback hell which will make the code bulkier and difficult to edit.
To tackle this problem and also provide some extra functionality we use promises, async, and await.
async and await method is just a better way to write the asynchronous type of code in a familiar synchronous code.
Thanks for your time. I hope this post was helpful and gave you a mental model for CallBacks and its implemention in JavaScript.
Subscribe to my newsletter
Read articles from Prasoon Abhinaw directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Prasoon Abhinaw
Prasoon Abhinaw
I am a passionate software engineer with over 16 years of hands-on experience in the tech industry. On this blog, I aim to share my expertise, insights, and love for engineering with all of you. Whether you're a seasoned developer or just starting your journey in the world of code, this blog is here to empower you and elevate your skills. What to Expect: Expect a diverse range of content, including in-depth tutorials, coding challenges, software development tips, algorithmic insights, tech reviews, and discussions on the latest trends in the tech sphere. We'll explore various programming languages, design patterns, data structures, and best practices to help you become a more proficient engineer.