Asynchronous JavaScript Guide with Callbacks, Promises and Async/Await

Asynchronous JavaScript Guide with Callbacks, Promises and Async/Await

Β·

9 min read

JavaScript is a single-threaded synchronous programming language. But still we are able to perform asynchronous tasks like rendering the page, making network calls, validating users and a whole bunch of other things all at once. This is nothing but Asynchronous Programming.

Now to understand JavaScript asynchronous programming better we'll be diving into the following concepts :

  • Difference between synchronous and asynchronous programming
  • Need for asynchronous programming
  • Different ways of writing async code in your JS apps
    • Callbacks
    • Promises
    • Async/Await

Synchronous v/s Asynchronous

Synchronous means as in the specified order and that is exactly how any of our JavaScript code runs i.e in a synchronous way. It starts executing from the very first line and proceeds with each subsequent instruction. Let's see an example for that

let a = 1
let b = 2

console.log("Synchronous code")

console.log("a -> ", a)
console.log("b -> ", b)

Now when you try to execute the above piece of code it will give the following output

Synchronous code
a ->  1
b ->  2

And this was exactly expected. Works perfectly fine !!

Now an asynchronous code on the other hand does not always give you the expected output. Asynchronous code often behaves in a way that the developer might not be expecting. Asynchronous code starts program execution from the first line and proceeds with each subsequent instruction but whenever it encounters any asynchronous function or code it'll split up and execute that asynchronous code separately from the rest of the code while still executing the main code. Let's see an example for this as well:

let a = 1
let b = 2

console.log("Asynchronous code")

setTimeout(()=>{
    console.log("inside timeout")
}, 2000)

console.log("a -> ", a)
console.log("b -> ", b)

On executing the above piece of code it'll produce the following result

Asynchronous code
a ->  1
b ->  2
inside timeout

This was a bit unexpected. This reason for such an output is the asynchronous flow of the code. The program starts from line 1 with initializing a and b with 1 and 2 respectively. Then it prints Asynchronous code. When it encounters setTimeout the program signals the browser telling that the code inside the setTimeout functions needs to be executed after 2000 milliseconds. To this the browser responds saying no problem, I'll execute it after 2000 milliseconds till then let me execute the remaining lines. Therefore after printing values of a and b once the call stack is empty it executes the code from setTimeout.

If you understood the flow of the above program find the output of this below piece of code and check your predictions with the actual output

let a = 1
let b = 2

console.log("Asynchronous code")

setTimeout(()=>{
    console.log("a inside timeout -> ", a )
}, 1000)

a = a+10

console.log("a -> ", a)
console.log("b -> ", b)

Can you determine what will be the output and the reason for such an output ?? You can share your answers in the comments with possible explanations [Hint: scope of let]

Need for Asynchronous Programming

Now that you've understood the difference between synchronous and asynchronous programming, the next question is the need for asynchronous programming.

This is relatively simple the explain. When developing an application the main objective is to provide a better user experience. You don't want the user to wait while the application first access data from server then authenticate the user and then load the data. That would be totally absurd.

Hence using asynchronous programming while the synchronous operations are executing on the application, all the asynchronous operations would be performing at the exact same time. Thus your application would be taking considerably less time to load providing a better user experience.

Callbacks

To achieve asynchronicity callbacks are among the most commonly used ways. Callbacks are simple functions which are used to notify the calling instance when an asynchronous code block has been executed and the result is available.

In layman terms, a callback function is a function passed into another function as an argument, which is then invoked inside the outer function to complete some kind of routine or action.

Think of cases when you want to add an eventListener to a button in your app or want to perform some operation on the data coming from a remote server. In both the cases you don't know when the user might click the button or when you'll receive the data. For such cases we can use callbacks which will execute some time later but we're not sure when that might be.

Let's understand this with an example. Think of an api which returns a json object with a movie name and director and we want to print the data for now. Now I've used a setTimeout function to achieve the same behavior.

const getMovieName = () => {
    setTimeout(()=>{
        return {name:"Man Of Steel", director:"Zack Snyder"}
    },2000)
}

const movie = getMovieName ()
console.log(`movie ->  ${movie.name}`)
console.log(`director -> , ${movie.director}`)

Now on executing this above piece of code we get TypeError: Cannot read property 'name' of undefined.
The reason is simple that output from the function would be returned after 2000 milliseconds. But till then the remaining lines get executed and result in TypeError.

Now if we use a callback function, the same code would look something like this

const getMovieName = (callback) => {
  setTimeout(() => {
     callback({ name: "Man Of Steel", director: "Zack Snyder" })
     console.log("movie fetched")
  }, 2000);
};

getMovieName((movie) => {
  console.log(`movie ->  ${movie.name}`);
  console.log(`director ->  ${movie.director}`);
});

console.log("fetching movie data")

Here when we call the function getMovieName, then in the function call itself we pass another function as an argument (since functions in JavaScript can be passed as arguments ). This passed function is our callback function. Then inside getMovieName after 2000 milliseconds the callback function is called with the json object and we get our desired result.

This is how callback functions can be used.

Now inside this callback function we could have passed another function and inside that function another function and so on...

firstFunction(args, function() {
  secondFunction(args, function() {
    thirdFunction(args, function() {
      // And so on…
    });
  });
});

Passing multiple callback functions might be the requirement of your application but it results in creating function tree that is too big and way too hard to refactor. This is called callback hell .

Callbacks can surely be used for smaller asynchronous operations. But it is not the optimal choice. Now with ES6, Promises were created to handle the issues created by callback hell.

Read more about callbacks πŸ‘‰ callbacks, MDN

Promises

A promise is commonly defined as a proxy for a value that will eventually become available.

In layman terms, a promise in javascript is very much similar to a promise in real life. A promise is like a commitment which will either be fulfilled/resolved or rejected. Promises were designed to handle asynchronous code without encountering a callback hell.

Once a promise has been called, it will start in a pending state. This means that the calling function continues executing, while the promise is pending until it resolves, giving the calling function whatever data was being requested.

The created promise will eventually end in a resolved state, or in a rejected state, calling the respective callback functions (passed to then and catch) upon finishing.

Creating Promises

The Promise API exposes a Promise constructor, which you initialize using new Promise().

new Promise((resolve, reject)=>{
    if(condition){
        resolve('Success')
    }else{
        reject('Failure')
    }
})

This promise contains an executor with two callbacks as its inputs namely resolve and reject. The resolve callback is called in case of a success and reject in case of a failure.

After a promise has been created, we can use it via chaining with .then() and .catch(). In case of a success the then block would be executed while the catch block would be executed for a failure.

promise
.then(successResponse => console.log(successResponse))
.catch(failResponse => console.log(failResponse));

You can add multiple then blocks in the chaining structure for multiple operations with the idea that result of one then block would be input for the next one. However there can be only one catch block.

Let's look at an example:

const searchMovie = (movie) => {
    return new Promise((resolve, reject)=>{
        setTimeout(()=>{
            if(movie === "Avengers"){
                resolve({movie, message:`Movie available to watch`}) 
            }else{
                reject({movie, message:"Sorry! Movie not Available"})
            }
        }, 2000)
    })
}

const playMovie = (response) => {
    console.log(response.message)
    return `YAY !! Watching ${response.movie}`
}

searchMovie("Avengers")
.then(searchResponse => playMovie(searchResponse))
.then(playResponse => {
    console.log(playResponse)
})

Here, I've defined two methods: searchMovie and playMovie. The searchMovie would return a promise depending upon the movie searched either a success or a failure. The playMovie would return a message string. Now we first call the searchMovie function which is an asynchronous function and will return a response based on the searched movie. Here it results in a success hence the first then block gets executed which take the promise response and call the other function which returns a message string and it gets printed in our second then block.

Now what would happen if I make the same promise call again but with a different movie name, like below :

searchMovie("Inception")
.then(searchResponse => playMovie(searchResponse))
.then(playResponse => {
    console.log(playResponse)
})

This results in an error saying :

(node:6124) UnhandledPromiseRejectionWarning: #<Object>
(node:6124) UnhandledPromiseRejectionWarning: Unhandled promise rejection

No need to worry on seeing this output. This is perfectly fine. Since for the other movie name our promise rejects it we need to handle a rejection separately using a catch block. Something like below :

searchMovie("Inception")
.then(searchResponse => playMovie(searchResponse))
.then(playResponse => {
    console.log(playResponse)
}).catch(err => {
    console.log(err.message)
})

Read more about promises πŸ‘‰ promises, MDN

Async/Await

An async function is nothing different or new but a mere change in handling the promises. Async-Await is just a sugar coated way of writing promises.

When using chaining in promises, though it was somewhat better than callbacks it still might get messy when managing huge chunks of code. With async/await the chaining using .then() and .catch() is totally eliminated.

Following is the syntax for async/await function:

async function someFunc(){
    let response = await getPromise();
    // do something 
}

An async function usually start with async keyword. It tells the browser that this is an asynchronous function and can be executed separately. Then there is await keyword. This tells that we are waiting for a promise and everything after this line will be executed once we've received a response.

Await can only exist inside an async function. Let's see an example for this as well

async function browseMovie(movie) {
    const searchReponse = await searchMovie(movie);
    console.log("Search response received");
    const playResponse = playMovie(searchReponse);
    console.log(playResponse); 
}

browseMovie("Avengers")

We're again using the same example with searchMovie and playMovie. But this time using async/await.

The first line inside the browseMovie function tells the browser to wait for a response from the promise and once the response is received we move ahead with function execution. However any code outside this function would be executing at normal rate. The only difference here from the promise is the syntax and ease of code refactoring.

Now for a function call like browseMovie("Inception") this results an error as encountered earlier. For error handling we can use try-catch block.

async function browseMovie(movie) {
  try {
    const searchReponse = await searchMovie(movie);
    console.log("Search response received");
    const playResponse = playMovie(searchReponse);
    console.log(playResponse);
  } catch (err) {
    console.log("Inside catch block");
    console.log(err.message);
  }
}

Read more about async/await πŸ‘‰ async/await , MDN

Conclusion

JavaScript executes asynchronous code in a non-blocking way which is great to ensure responsiveness of the application. However, when dealing with asynchronous code we need to handle it in a special way to be able to deal with the results which the code is delivering. This can be achieved through Callbacks, Promises and Async/Await.

Β