“A single-threaded, non-blocking, asynchronous, concurrent programming language with many flexibilities”.
This is how freeCodeCamp defines JavaScript.
Sounds great, right? Well, read it again.
JavaScript is supposed to be single-threaded
and asynchronous
simultaneously. Which, when you think about it, sounds like a contradiction.
But don’t worry, in this article we’re going to go under the hood, looking specifically at the possibilities of asynchronous programming in JavaScript with Callback, Promises, and Async/Await, so you can see how the two terms interact and co-exist with one another.
The Synchronous Part of JavaScript
In JavaScript, functions
are first-class citizens.
You can create a function, update it, pass it as an argument to another function, return it from a function, and assign a function to a variable.
In fact, the JavaScript programming language would be unusable without the existence of functions; we assign many repetitive tasks to a function and they execute on demand.
//Define a function
function doSomething() {
// All tasks to do
}
// Invoke or Execute the function
doSomething();
JavaScript executes the function sequentially, line by line, and its engine maintains a stack data structure called Function Execution Stack
(also known as Call Stack) to track the execution of the current function.
When the JavaScript engine invokes a function, it adds the function to the stack to start the execution. If that function calls another function within it, the second function also gets added to the stack, and executes.
Following the execution of the second function, the engine removes it from the stack and continues the execution of the first function. Once the first function has executed, the engine also removes that from the stack. This continues until the stack becomes empty.
The Function Execution Stack or Call Stack
Everything that happens inside the Call Stack is sequential, and that makes JavaScript synchronous. The main thread of the JavaScript engine takes care of everything in the call stack first, before it starts looking into other places like queues and Web APIs (don’t worry, we’ll dig into these in a while).
The Asynchronous Part of JavaScript
Now, let’s understand how the term ‘asynchronous’ applies to JavaScript.
In general, asynchronous
refers to things that don’t occur at the same time. The synchronous behaviour of JavaScript is fine, but it may cause issues when users want dynamic operations, such as fetching data from the server or writing into the file system.
We don’t want the JavaScript engine to suspend its execution of the sequential code when it needs to take care of the dynamic behaviours. In fact, we want the JavaScript engine to execute the code asynchronously.
The asynchronous behaviors in JavaScript can be broken down into two factors:
- Browser APIs or Web APIs : These APIs include methods like setInterval and setTimeout, and offer asynchronous behaviours.
- Promises : This is an in-built JavaScript object, and helps us deal with asynchronous calls.
In this article, we’ll focus on the latter factor to understand JavaScript promises and how they work. But before that, let’s learn about the predecessor of JavaScript promises – that is, the Callback
functions.
What is a Callback Function, and How does it Work?
We’ve already noted that JavaScript functions are a crucial aspect of the JavaScript language and a function can take another function as an argument. This is a powerful feature that was introduced to handle certain operations that may occur at a later point in time, i.e. asynchronously
.
Now let’s take a look at the following code snippet.
We have a function, doSomething()
, which takes a function, doSomethingLater()
, as an argument and invokes it based on certain conditions. Here, doSomething() is the Caller Function
and doSomethingLater() is the Callback Function
.
Later we’ll invoke the caller function, passing the callback function as an argument.
function doSomething(doSomethingLater) {
// Code doing something
// More code...
if(thatThingHappen) {
doSomethingLater()
} else {
// Continue with whatever...
}
}
// Invoke doSomething() with a callback function
doSomething(function() {
console.log('This is a callback function');
});
This callback function methodology was widely used to perform asynchronous calls in JavaScript. Take this example:
function orderBook(type, name, callback) {
console.log('Book ordered...');
console.log('Book is for preparation');
setTimeout(function () {
let msg = `Your ${type} ${name} Book is ready to dispatch!`;
callback(msg);
}, 3000);
}
The orderBook
method takes three arguments:
- Type of the book.
- Name.
- A callback function.
We have simulated the asynchronous behavior using the Web API function setTimeout, and we invoke the callback function after a delay of 3 seconds.
Now, take a look at the invocation of the caller function.
orderBook('Science Fiction', 'Blind Spots', function(message){
console.log(message);
});
Here, we want to order a science fiction book named Blind Spots and we’ll get a message from the callback function once the book is ready to dispatch. Here is the output.
The output of the book ordering app
So we’ve seen that the JavaScript callback function is well-suited to handle the asynchronous call’s success or error cases, and report it to the end users. But it also introduces problems as your application grows.
When you have to make multiple asynchronous calls and handle them using the callback functions, this will lead to a situation where you will invoke one callback function inside another, and eventually creates a Callback Hell
– or, as it’s more commonly known, a Callback Pyramid
.
Callback Hell – Let’s not get into it.
A code like the one above is tricky to read, difficult to debug, and may introduce further errors.
But don’t panic: there are ways out of the callback hell, and one of the most effective is by using JavaScript Promises.
What are JavaScript Promises?
JavaScript promises are the special objects that help deal with the asynchronous operations. You can create a promise using the constructor syntax:
const promise = new Promise(function(resolve, reject) {
// Code to execute
});
The Promise
constructor function takes a function as an argument, and this is called the executor function
.
// Executor function
function(resolve, reject) {
// Your logic goes here...
}
The code inside the executor function runs automatically when the promise is created.
The executor function takes two arguments, resolve
and reject
. These are the callback functions provided by the JavaScript language internally.
The executor function call either resolves or rejects callback functions. resolve
handles the successful completion of tasks, while reject
handles cases where there is an error and the task is incomplete.
The Promise
constructor returns a promise object, with two important properties:
-
state : The
state
property can have the following values:- pending: When the executor function starts the execution.
- fulfilled: When the promise is resolved.
- rejected: When the promise is rejected.
-
result : The
result
property has the following values:- value: When resolve(value) is called
- error: When reject(error) is called
- undefined: When the
state
value ispending
.
You cannot access these properties in your code directly. However, you’ll be able to debug them using debugger tools.
Inspecting promises with the debugger devtool
JavaScript Promises – resolve and reject
Here’s an example of a JavaScript promise that resolves immediately with the message “task completed”.
let promise = new Promise(function(resolve, reject) {
resolve("task complete");
});
Now here’s an example of a promise that rejects immediately with the message “error: task incomplete”.
let promise = new Promise(function(resolve, reject) {
reject(new Error('Error: task incomplete'));
});
Handling Promises Using the Handler Functions
So far we’ve learned about the executor function
, where we write the code to make the asynchronous call and resolve/reject based on the outcome.
But what happens when we resolve or reject from an executor function? Who uses it? What do we do with the promise object after that? How do we handle it in our code when resolve/reject takes place?
So many questions, right?? Let’s try to get answers using the diagram below:
Promise executor and consumer functions
When the executor function is done with its tasks and performs a resolve (for success) or reject (for error), the consumer function
receives a notification using the handler methods like .then()
, .catch()
, and .finally()
.
We invoke these methods on the promise object.
promise.then(
(result) => {
console.log(result);
},
(error) => {
console.log(error);
}
);
Here’s an example using the .catch()
method:
new Promise((resolve, ¸) => {
reject("Error: Task incomplete!");
}).catch((error) => console.log(error));
Promise Chain – A Better Alternative than Callback Functions
The .then()
handler method returns a new promise object. This means you can now call another .then()
method on the returned promise object.
Also, the first .then()
method returns a value that JavaScript passes to the next .then()
method, and so on up to a Promise Chain
.
Let’s dig into this with an example. Read the following code snippet carefully:
let promise = getPromise(ALL_BOOKS_URL);
promise.then(result => {
let oneBook = JSON.parse(result).results[0].url;
return oneBook;
}).then(oneBookURL => {
console.log(oneBookURL);
}).catch(error => {
console.log('In the catch', error);
});
- Assume the
getPromise()
method returns a promise. - Now we have to handle this promise. So, we call the
.then()
handler method, which receives the result value as the callback. - Next, we parse the value, extract the first URL from the response and return.
- When we return something from the
.then()
method, it returns a promise. So, we can again handle this using another.then()
method. Thus a promise chain has formed.
If we had to handle the same with the Callback
function we learned earlier, we would have created a callback pyramid. The above code, with promises, is much more readable and easier to debug.
A Further, Easier Way: Async/Await Keywords
JavaScript provides two keywords, async
and await
, to make the usages of promises even easier.
These keywords are syntactic sugar on the traditional JavaScript promises to help developers provide a better user experience while writing less code.
- We use the
async
keyword to return a promise. - We use the
await
keyword to handle a promise.
When a function performs the asynchronous operation, we add the async
keyword as shown in the example below. The JavaScript engine will know that this function may take a while to get us the response, and may result in a value or error.
async function fetchUserDetails(userId) {
// pretend we make an asynchronous call
// and return the user details
return {'name': 'Tapas', 'likes': ['movies', 'teaching']};
}
When we invoke the fetchUserDetails()
function, it returns a promise like this:
The output of executing an async function
Because the above function returns a promise, we need to handle it. Here comes the await
keyword.
const user = await fetchUserDetails();
console.log(user)
When we invoked the function, we have used the await
keyword. It will make the execution of the async function wait until the promise is settled, either with a value (resolve) or with an error (reject).
The returned user object after the resolved promise
Question for you: How would you handle the above code of execution with the plain JavaScript promises? You may be doing something like this:
const userPromise = fetchUserDetails(123);
userPromise.then(function(user) {
console.log(user);
});
As you’ll see yourself, the async/await
keyword certainly makes the code easier to read than the plain JavaScript promises.
A word of caution here, though: you cannot use the await
keyword with a regular, non-async function. You will get an error when you try doing so.
Before We Go…
We hope you’ve found this article insightful and it gives you the walkthrough and comparison between callback
functions, promises
, and the async/await
keywords. As a web developer, you must know all these usages even though you use more of async/await in your code than the other counterparts.
JavaScript asynchronous programming is an interesting subject and, with practice, you can master it really well to face interviews with confidence. So make sure you do lots of practice using callback, promises, and async/await examples. To help you with that, here is a repository with many quizzes and examples of asynchronous programming that you may want to try out:
Also, if you are keen to explore this subject in greater detail, take a look at this playlist on YouTube.
Top comments (0)