Handling “A”-synchronousity in JS

📄 Table of Contents

History

Thankfully, then (or should we say .then()) came Promises. They offered a much more organised alternative to callbacks and most of the community quickly moved on to using them instead.

Now, with the most recent addition of Async/Await, writing JavaScript code is about to get even better!

So in a nutshell, in JavaScript, we have a few different ways to handle asynchronous-ity namely callback functions, Promises, and async-await.

[1]. Callbacks functions

[2]. Promises (since ES6) => resolved Callback Hell.

[3] Async/Await (since ES8) => work on group of Promises + generators

Callbacks

The timing of the callback function execution entirely depends on the calling function’s mechanism. This is the sole basis of the asynchronous or non-blocking behavior of the Node JS framework.

How to define a custom callback

When a callback function is passed as an argument to another function, only the function definition is passed.

Passing a custom callback as an argument

A callback function is a function you provide that will be executed after completion of the async operation. Let’s create a fake user data fetcher and use a callback function to do something with the result.

The Fake Data Fetcher

In order to be able to actually do something with our fakeData, we can pass fetchData a reference to a function that will handle our data!

Let’s create a basic callback function and test it out:

After 300ms, we should see the following logged:

Promises

In a synchronous execution, you don’t need states, per se. Your code either successfully execute or fail to execute. In asynchronous code, we execute, wait for callbacks and decide if its success or failure and continue with the synchronous code execution.

Promises “saves you from Callback hell”.

Syntax

Basic Promises Syntax

Note: Here, the .then() method accepts both resolved and rejected callbacks, but catch accepts only rejected callbacks. In practice, always use the then method for handling resolved callbacks and catch for rejections to keep things cleaner and for proper funnelling of errors at a single exit point.

A promise is an object that may produce a single value some time in the future: either a resolved value, or a reason that it’s not resolved (e.g., a network error occurred).

  • Promises can either resolve or reject. When a Promise resolves, you can handle its returned value with .then then method. If a Promise is rejected, you can use the .catch the error and handle it.
  • A promise may be in one of 3 possible states: fulfilled, rejected, or pending. Promise users can attach callbacks to handle the fulfilled value or the reason for rejection.
  • Both .then and .catch will return a new promise., which means that promises can be chained.
  • Promises solved earlier Callback-Hell issue.
  • The main difference between Promise.allSettled and Promise.all and Promise.race is that the allSettled does NOT short-circuit.
  • .all and .race DO short-circuit:
  • .all stops evaluation if any promise is rejected and .race stops when any promise is either fulfilled or rejected.
  • Promise.allSettled() method returns a promise that resolves after all of the given promises have settled down, which means they are either resolved or rejected, with an array of objects that each describe the outcome of each promise.

◉ The Fake Data Fetcher (with Promises)

Our new fetchData function can be used as follows:

If fetchData successfully resolves (this will happen 90% of the time), we will log our user data as we did with the callback solution. If it is rejected, then we will console.error the error message that we created (“Fetch failed!“)

One nice thing about Promises is you can chain then to execute subsequent Promises. For example, we could do something like this:

Furthermore, we can pass an array of Promises to Promise.all to only take action after all Promises are resolved:

In this case, if both Promises are successfully resolved, the following will get logged:

Basic Promise With A Single .then() Method

Basic Promises with a single then method

Once the resolve method of the Promise constructor is called the resolve handler picks up the resolved value as an argument and prints it. Interestingly the then method doesn’t always behave this way in a Promise. Let’s see more of thens in the next section.

Promises With Multiple .then() Methods

.then method following a Promise constructor

.then() method following another ‘then’ returning a value/reference

Then methods executing synchronously (not recommended)

Once the promises are resolved the then methods linked to the promises start executing one after the other, here you can add any number of thens and it can go on forever. Literally forever. This is similar to a code inside a really long function, executing one after the other. Although this is asynchronous by-nature, this can be a bit confusing for beginners.

Note: one interesting behaviour of this is that when you do not return anything from the previous then method, x is undefined.

.then() method following another ‘then’ returning a Promise

Promise Chaining

Let’s consider we have two promise blocks, (a Promise block is a Promise constructor followed by its own then methods). And we want one block to execute after the other, what do we do? We make use of the asynchronous behaviour of the then method.

When a then method returns a promise, the subsequent executions of the then are suspended until this promise block is resolved.

Let’s look at a more detailed example to understand further. In the following image, you can see clearly see three promise blocks and let’s understand how they form a promise chain.

Chaining promises

Explanation

  1. Similarly, promise2 and promise3 will also be pending, which will get resolved after 2 and 3 seconds respectively. (we will see the order of resolutions in coming sections, don’t worry about it now)
  2. At the1st second, promise1 is resolved and the resolved value of ‘first’ appears in the resolve handler.
  3. The resolve handler returns another promise called promise2. Now the subsequent the thens following would have to wait till promise 2 is resolved.
  4. The promise2 executes all the attached then methods, before continuing to execute the promise1’s then methods. The value returned by promise2 block’s last then method appears in the next occurring then of the promise1 block.
  5. Now again the execution is suspended till promise3 is resolved before finishing the whole execution.

At first, the example might be a bit harder to understand, give it a good 10 minute read to wrap your head around it. Let it sink in before you move on to the next section.

Promise Pyramid or Nested Promises

Although there are different ways of achieving this, to keep it simpler we can use something called as Nested Promises. These nested promises will have access to variables from its outer scope, and are otherwise called as promise-pyramids, which will grow to the right. If not done correctly, this promise pyramid can eventually lead to something called as Promise-Callback Hell.

Let’s look at the problems accessing variables in promise-chaining below,

Problem with promise chaining

To ensure that all the consecutive promise blocks have access to all the parental scope variables, we can nest promises. Let’s look a simple two level nesting of promises to understand it better,

Nested promises

In the nested promises, you can see that promise2’s then method resolves and prints both the value from the outer scope and the inner scope.

A very important point to keep in mind is that during nesting sometimes we forget to return the nested promises, which will lead to an anti-pattern where your resolves/rejects will not bubble up to the top. Always!! I say always return a promise when its chained or nested.

( You can try rejecting the promise2 and verify whether catch method successfully catches the error or not, take it as a challenge — with and without a return statement before promise2)

Sometimes nesting promises become inevitable which will inevitably lead to the callback hell we talked about, hence to tackle this issue ES7 has come up with Async/await. Be sure to check it out when you have time.

Order of Execution of Promise Blocks

There are two important points to remember about the promise execution cycle in a normal promise block and in a chained/nested promise block.

  1. A promise once created races towards resolve/reject. If its resolved/rejected it changes its state to resolved and executes the attached then/catch method.
  2. In a promise chaining or nesting when you return a promise inside a then method, and if the returned promise is already resolved/rejected, it will immediately call the subsequent then/catch method, if not it will wait.

Let’s look at an example to understand better,

Order of execution of promise blocks

Explanation

  1. promise1, promise2 and promise3 are initialised and they are racing towards resolve/reject in time. Currently, it is zeroth second.
  2. In the1st second, all three promises are pending.
  3. In the 2nd second, promise2 has resolved.
  4. In the 3rd second, promise1 has resolved and the then handler is executed. In the then handler a resolved promise2 is returned. (a resolved promise holds resolved value and has a resolved state)
  5. Since promise2 is already resolved it immediately calls the next then method attached, without waiting.
  6. In the 3rd second the promise 3 is still pending because it needs two more seconds to be resolved.
  7. After 5th second all three promises are resolved.

Catching errors in Promises

But throwing errors has its limitations in asynchronous coding, when we throw an error inside an async block of code it will never be caught. Alternatively, rejecting and returning a promise properly should work seamlessly without any issues and its considered the best practice.

Also, you can reject a promise in any of its attached then methods by simply returning a Promise.reject(customErrorValue);

Promise.all() vs Promise.race()

Anti-patterns

Then methods with no-returns

Similarly, when we use a promise chaining, we need to return the promises. When we don’t return the promises, the consecutive .then methods will not realise that it needs to wait for a promise in the sequence and simply assumes it as another line of code to be executed, there by losing its async nature.

Promise chaining with/without return statement

Note: Only in the promise constructor block you do not have to explicitly return a resolve/reject. Apart from that, in the consecutive then methods, every value and promise chained needs to be returned, for accessing it further down the block.

Bonus Coding Challenge

Hint: Use for loop to chain promises :).

Conclusion

Async/Await

  • It is build on top of Promises and is compatible with all existing Promise-based APIs.
  • Async/await is actually just syntax sugar built on top of promises. It cannot be used with plain callbacks or node callbacks.
  • Async/await is, like promises, non blocking.
  • Async/await makes asynchronous code look and behave a little more like synchronous code. This is where all its power lies.

The name comes from async and await - the two keywords that will help us clean up our asynchronous code:

Async

async function someName(){...}

The word “async” before a function means one simple thing: a function always returns a promise.

Even though you do not return a promise, JavaScript converts it to a Promise.

async function myFunc() {
return 1
}

The above code is equivalent to this.

function foo() {
return Promise.resolve(1)
}
  • Any async function always returns a promise implicitly.
  • When called, async functions resolve with whatever is returned in their body.
  • Async functions enable the use of await. One async function can have more than one ‘await’ expressions.

Await

var result = await someAsyncCall();

  • When placed in front of a Promise call, await forces the rest of the code to wait until that Promise finishes and returns a result.
  • Await works only with Promises, it does NOT work with callbacks.
  • Await can only be used inside async functions.

Here is a simple example that will hopefully clear things up:

Let’s say we want to get some JSON file from our server. We will write a function that uses the axios library and sends a HTTP GET request to https://tutorialzine.com/misc/files/example.json. We have to wait for the server to respond, so naturally this HTTP request will be asynchronous.

same implementation using Promises & Async/await

It’s pretty clear that the Async/Await version of the code is much shorter and easier to read. Other than the syntax used, both functions are completely identical — they both return Promises and resolve with the JSON response from axios. We can call our async function like this:

setTimeout along with async-await

getJSONAsync().then( function(result) {
// Do something with result.
});
async function f() {

let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("done!"), 1000)
});

let result = await promise; // wait until the promise resolves (*)

alert(result); // "done!"
}

f();

Cases where Promises still scores over Async-await

No, not at all. When working with Async/Await we are still using Promises under the hood. A good understanding of Promises will actually help you in the long run and is highly recommended.

There are even use cases where Async/Await doesn’t cut it and we have to go back to Promises for help.

[1]. Making multiple IN-dependent A-synchronous calls

If we try and do this with async and await, the following will happen:

async function getABC() {
let A = await getValueA(); // getValueA takes 2 second to finish
let B = await getValueB(); // getValueB takes 4 second to finish
let C = await getValueC(); // getValueC takes 3 second to finish
return A*B*C;
}

Each await call will wait for the previous one to return a result. Since we are doing one call at a time the entire function will take 9 seconds from start to finish (2+4+3).

This is NOT an optimal solution, since the three variables A, B, and C aren't dependent on each other. In other words we don't need to know the value of A before we get B. We can get them at the same time and shave off a few seconds of waiting.

To send all requests at the same time a Promise.all() is required. This will make sure we still have all the results before continuing, but the asynchronous calls will be firing in parallel, not one after another.

async function getABC() {
// Promise.all() allows us to send all requests at the same time.

let results = await Promise.all([ getValueA, getValueB, getValueC ]);
return results.reduce((total,value) => total * value);
}

This way the function will take much less time. The getValueA and getValueC calls will have already finished by the time getValueB ends. Instead of a sum of the times, we will effectively reduce the execution to the time of the slowest request (getValueB - 4 seconds).

[2]. Handling Errors in Async/Await

async-await using try-catch

The .catch clause will handle errors provoked by the awaited asynchronous calls or any other failing code we may have written inside the try block.

If the situation requires it, we can also catch errors upon executing the async function. Since all async functions return Promises we can simply include a .catch() event handler when calling them.

async-await WITHOUT try-catch

It’s important to choose which method of error handling you prefer and stick to it. Using both try/catch and .catch() at the same time will most probably lead to problems.

Browser Support

Experience with Front-end Technologies and MERN / MEAN Stack. Working on all Major UI Frameworks like React, Angular.