Callback functions, Promises, and Async Await are concepts fundamentally used by JavaScript to handle deferred operations.
Sometimes an operation could be synchronous (blocking) or asynchronous (non-blocking). Let us take a good look at the difference between these two types of operations.
Difference Between Synchronous and Asynchronous Operations
Let’s consider this first scenario …
Imagine your car broke down in the middle of the highway, far from where you live. Then you managed to go look for some car mechanic in the nearest town. Luckily, you found one and he comes and start working on your car. You, as the owner of the vehicle, normally won’t be able to do anything else, you have to stay put and monitor and observe all that this “strange” mechanic is doing to your car. Ring a bell?
Let’s again consider a second scenario …
Imagine now you are at home, and you have a trusted mechanic that services your car. Now for some reason your car broke down on your way to work. Since your mechanic is within the town that you reside in, you call him and handover the car to him for repairs, while you go about your work and do other things.
Now the first scenario illustrated above refers to a Synchronous, blocking or single threaded event. You had to wait for the mechanic to finish repairing the car before you could do anything else.
The second scenario refers to an Asynchronous or non-blocking event because you did not have to wait and supervise the mechanic because he’s your trusted guy. You just left the car for him and went about your other operations.
These scenarios illustrate the fundamental difference between Synchronous and Asynchronous events.
One important point to note is that in a single threaded system, operations are usually handled using an event or a message queue. So when a program is being executed synchronously, the thread will wait until one instruction is carried out and executed before moving on to the next one, while in an Asynchronous execution even if the first instruction is not yet implemented, the program can still implement other instuctions.
Having made the above establishment, in JavaScript, there are different ways to handle asynchronous events. These include callbacks, promises and async/await.
Callbacks
A callback function is a function that is passed to another function so that when the first function is executed, the second one which is passed to first one will also execute.
This means that we can pass functions as parameters to other functions and call these passed functions in the other functions.
Let’s consider this example …
function printUser(){
console.log("John");
setTimeout(function() { console.log("James"); }, 2000);
console.log("Jude")
}
printUser();
Now, if this were to be a synchronous event, we would get the following result:
John, James, Jude.
But since this is an asynchronous code, we would get the result:
John, Jude, James
This is so because we have a “setTimeout” method which is built in to JavaScript, which executes a function after a specified duration which in this case is 2 seconds.
So the callback is the function that is passed as the argument to setTimeout method.
Using Arrow Functions For Callbacks
Sometimes, depending on your preference, there are times you might want to use Arrow functions to implement a callback, and the above code can be re-written as follows:
function printUser(){
console.log("John");
setTimeout(() => { console.log("James"); }, 2000);
console.log("Jude")
}
printUser();
Running this code, we would still get the same output as previously seen.
Callback Hell
While callbacks are a good way to execute a chain of a given function, there is every possibility of confusion when you start nesting functions inside functions and inside functions. This can make the code difficult to read and can be generally frustrating. Most people call this type of frustration ‘callback hell’
Promises to the Rescue
when we make a promise in real life, we are registering that we are going to do something in future, maybe buy a car or build an apartment.
But it is also possible that when the time comes to fulfil such promise, the promise will either be kept or it won’t. Whatever happens, there must of necessity be an outcome from a promise.
This is the same thing that happens with promises in JavaScript. When we define a promise in JavaScript, it’s either it would be resolved when the time comes or it won’t.
A promise is used to handle the asynchronous outcome of an operation.
With JavaScript, it is not necessary for us to wait for an asynchronous block of code to completely execute before other synchronous parts of the code can run. With Promises, we can defer the execution of a code block until an async request is completed. This way, other operations can keep running without interruption and without overloading the CPU.
Three (3) States of Promises in JavaScript
Since Promise is an object, there are 3 states that this object goes through.
- Pending. This is the initial state before the Promise succeeds or fails.
- Resolved. This refers to a completed Promise, one that is kept and has been resolved. Here, we usually move on to the next logic of the program
- Rejected: This refers to a failed Promise, one that has been rejected. Here is where we throw an error and tell the user what is really going on, and why the said Promise couldn’t be kept.
An example of this is requesting data from a server through an API endpoint. It will first be on pending mode until a response is gotten. It is possible that this received response will either be a favourable one (Resolved) or an Unfavourable one (Rejected).
Creating a Promise
When creating a Promise, we generally use a constructor. The Promise also takes in two parameters, one for successful completion, and the other for a failed operation.
const myPromise = new Promise((resolve, reject) => {
// condition
});
Let’s see an example
const myPromise = new Promise((resolve, reject) => {
const condition = true;
if(condition) {
setTimeout(function(){
resolve("Promise is resolved!"); // fulfilled
}, 2000);
} else {
reject('Promise is rejected!');
}
});
Let’s see an example Using Fetch API
fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits', {
method: 'GET',
})
.then((response) => response.json())
.then((responseJson) => {
console.log(responseJson)
})
.catch((error) => {
console.log(error)
});
};
Async/Await
Await is what most people have described as syntactic sugar used for Promises. The idea is to make your asynchronous code look more like synchronous/procedural code, which is easier for humans to understand.
When using this method, the function is wrapped with the “async” keyword. This lets JavaScript know that you want the function to be an Async one and that you also want to be able to use the Await keyword.
This means that you cannot of necessity use Await in a function without first putting the that function in an Async wrapper.
Example
const api_url =
"https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits";
// Defining async function
async function getapi(url) {
// Storing response
const response = await fetch(url);
// Storing data in form of JSON
var data = await response.json();
console.log(data);
if (response) {
//do logic here
}
show(data);
}
// Calling that async function
getapi(api_url);
Conclusion
Hopefully, we now get the concept of callbacks, promises and async/await and how to use them in our program. These concepts usually work with asynchronous requests and is heavily used for fetching or posting data to a server via an API endpoint.