Asynchronous JavaScript

Asynchronous JavaScript

Everything you need to know about Asynchronous JavaScript

The asynchronous nature of JavaScript is the most confusing and the most mistaken topic. I am here to help you understand every bit of it deeply. So fasten your seatbelt blog gonna be a big one but be patient It will solve most of your problems related to the asynchronous nature of JS.

Prequisites

Knowledge of basic JavaScript and Syntax around that and else everything will be covered in it.

Callbacks

There are many functions provided by JavaScript that can handle the asynchronous actions (actions that we initiate now but finishes later on ). For instance, setTimeout is one of them.

function loadScript(src) {
  // creates a <script> tag and append it to the page
  // this causes the script with given src to start loading 
and run when complete

  let script = document.createElement('script');
  script.src = src;
  document.head.append(script);
}
// load and execute the script at the given path
loadScript('/my/script.js');

It helps us to insert a new dynamically created, tag <script src=".....">. With the given src browser automatically loads it and starts executing when complete.

So think of it now we want to use the functions inside the script as soon as it loads. But if we immediately invoke the functions they would not work reason being the scripts need some time to be loaded.

loadScript('/my/script.js'); // the script has "function newFunction() {…}"

newFunction(); // no such function!

So to solve this problem we can think of putting a condition that when this function loadScript finishes then only the newFunction initialize. So for that, we will use the callback function as the second argument like below.

function loadScript(src, callback) {
  let script = document.createElement('script');
  script.src = src;

  script.onload = () => callback(script);

  document.head.append(script);
}

onLoad is an event that fires when a source is loaded. You can read more about it here. Now you can write newFunction like this.

loadScript('/my/script.js', function() {
  // the callback runs after the script is loaded
  newFunction(); // so now it works
  ...
});

Callback in callback

If we want to load one more script so, for that too we will do it with the callback only. Something like this-

loadScript('/my/script.js', function(script) {

  loadScript('/my/script2.js', function(script) {

    loadScript('/my/script3.js', function(script) {
      // ...continue after all scripts are loaded
    });
  });
});

Handling errors in the callbacks

For this we will use the onerror like onload something like given below.

function loadScript(src, callback) {
  let script = document.createElement('script');
  script.src = src;

  script.onload = () => callback(null, script);
  script.onerror = () => callback(new Error(`Script load error for ${src}`));

  document.head.append(script);
}

Pyramid of doom

The reason we use promises is Pyramid of Doom. For two or three asynchronous calls callback seems pretty nice but as we need more code to be performed one after another it starts becoming somewhat hectic to use. // center-aligned image Pyramid of Doom

Promise

Think of your son asking for some gifts and you promised him that you will give him what he asked for. There are 3 states of that promise 1)Pending Initially your promise will be in a pending state Until you have failed or you delivered what you promised. 2)Fulfilled You give him what he asked for then your promise is fulfilled as you kept it. 3)Rejected For some reason you failed to get him what he wanted from you. Then you in a way rejected the promise. This is what the code analogy explains too. We will discuss it more in detail below. Because promises are even more complex than this simple explanation.

Constructor syntax for the promise object

let promise = new Promise(function(resolve, reject) {
  // executor (the producing code, "singer")
});

This callback function in the above syntax is known as the Executor. Resolve and Reject are provided by the JavaScript itself. We only write the logic inside the Executor. When the executor obtains the result, be it soon or late, doesn’t matter, it should call one of these callbacks:

1)resolve(value) — if the job is finished successfully, with result value. 2)reject(error) — if an error has occurred, the error is the error object.

The promise object returned by the new Promise constructor has these internal properties:

  • state — initially "pending", then changes to either "fulfilled" when resolve is called or "rejected" when reject is called.

  • result — initially undefined, then changes to value when resolve(value) called or error when reject(error) is called. promise states

Consumers

Promise serves as the link between the Executor and the consuming functions. Consuming functions will have the Error or the Result. We can subscribe to the functions with the help of the catch, then, finally.

then

The most fundamental and used one is then.

//Syntax
promise.then(
  function(result) { /* handle a successful result */ },
  function(error) { /* handle an error */ }
);

-The first argument is a function that will run when the promise is resolved. -The second argument is a function that will run when the promise is rejected. example-

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

// resolve runs the first function in .then
promise.then(
  result => alert(result), // shows "done!" after 1 second
  error => alert(error) // doesn't run
);

If we are only interested in the successful completions then we can provide only one argument to then.

catch

We can use catch when we are only interested in the errors. The catch is a complete analog of the .then(null,f). It is just a shorthand for this.

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

// .catch(f) is the same as promise.then(null, f)
promise.catch(alert); // shows "Error: Whoops!" after 1 second

finally

Finally is very useful in the sense that there are some actions to be taken even if the promise is rejected or completed. It is good to have for cleanup no matter what the outcome is.

new Promise((resolve, reject) => {
  /* do something that takes time, and then call resolve/reject */
})
  // runs when the promise is settled, doesn't matter
 //successfully or not
  .finally(() => stop loading indicator)
  // so the loading indicator is always stopped before 
//we process the result/error
  .then(result => show result, err => show error)

Promises Chaining

Promises chaining helps us to achieve the task in which we have to perform multiple asynchronous tasks one by one. For ex-

new Promise(function(resolve, reject) {

  setTimeout(() => resolve(1), 1000); // (*)

}).then(function(result) { // (**)

  alert(result); // 1
  return result * 2;

}).then(function(result) { // (***)

  alert(result); // 2
  return result * 2;

}).then(function(result) {

  alert(result); // 4
  return result * 2;

});

This is the flow where the first promise will be solved and then the second and it goes on.

Flow of promise chaining Whole things work as promise returns a value and .then next to it returns a promise, so that we can call next .then on it.

Fetch

Programmers need to get the data from the api which is often done by the fetch method.

//Syntax for ex-
fetch('/article/promise-chaining/user.json')
  // .then below runs when the remote server responds
  .then(function(response) {
/* response.text() returns a new promise that resolves with 
the full response text
 when it loads*/
    return response.text();
  })
  .then(function(text) {
    // ...and here's the content of the remote file
    alert(text); // {"name": "iliakan", "isAdmin": true}
  });

Error Handling with Promises

Chains are pretty efficient in handling the errors for that just put a .catch. It does not have to be immediate. The most convenient practice is to place the .catch in the last of the chain so it can catch any error from any promise. As it doesn't have to be immediate after the promise which created the error.

Unhandeled Rejections

It is important to understand what will happen if we will not handle the errors of the promises then in such a case, the js engine creates a global object and handles such errors, and shows them in the console.

Promise API

There are 6 APIs for the promise.

Promise.all

Let's say we have multiple promises and we want to perform several actions only when they all are completed for that only we use let promise = Promise.all(iterable);. The new promise resolves when all listed promises are resolved, and the array of their results becomes its result.

//Syntax example
Promise.all([
  new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1
  new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2
  new Promise(resolve => setTimeout(() => resolve(3), 1000))  // 3
]).then(alert);

Promise.allSettled

This is a comparatively new addition to the browser and introduced to tackle the problem of promise.all() where promise.all will cancel all the promises if one fails promise.allSettled() just waits for all promise to settle irrespective of their respective status of being rejected or successful.

  • {status:"fulfilled", value:result} for successful responses,
  • {status:"rejected", reason:error} for errors.

Promise.race

It's similar to promise.all only difference it's just waiting for the first promise to be settled(it can be successful or rejected).

Promise.any

Similar to promise.race() but It returns the first promise which is fulfilled. If all promises are rejected then it returns a special kind of error object AggregateError- It stores all promise errors in its errors property.

Promise.resolve

Promise.resolve(value) creates a resolved promise with the result value.

Promise.reject

Promise.reject(value) creates a rejected promise.

Microtasks

Promise tasks are always asynchronous. Even if a promise is immediately resolved the code below the promise will resolve prior to it.

Microtasks Queue

For proper management of the asynchronous tasks ECMA specifies an internal queue PromiseJobs for now v8 naming is Microtasks Queue.

-The queue is first-in-first-out: tasks enqueued first are run first.

  • Execution of a task is initiated only when nothing else is running.

Or to put it more simply promise are exectued once all the code is done so for that they are put in a specific queue so they are not executed yet. When the JavaScript engine becomes free from the current code, it takes a task from the queue and executes it. There is one more queue to understand more about it click here

Async/await

There's a better syntax to work with promises that are async/await. So they work with each other for working on the promise. So, async ensures that the function returns a promise, and wraps non-promises in it.

The keyword await makes JavaScript wait until that promise settles and returns its result.

async function f() {
  let promise = Promise.resolve(1);
  let result = await promise; 
}

The function will wait until the statement in the await is fulfilled.

Conclusion

  1. Callbacks are the functions that are passed in other functions as arguments.
  2. Pyramid of doom is the problem with the callback's repetitive chaining which we can ignore only after using promise.
  3. Promise handling is always asynchronous, as all promise actions pass through the internal “promise jobs” queue, also called the “microtask queue” (V8 term).
  4. If we need to guarantee that a piece of code is executed after .then/catch/finally, we can add it into a chained .then call.
  5. Promise.all(promises) – waits for all promises to resolve and returns an array of their results. If any of the given promises rejects, it becomes the error of Promise.all, and all other results are ignored.
  6. Promise.allSettled(promises) (recently added method) – waits for all promises to settle and returns their results as an array of objects with: status: "fulfilled" or "rejected" value (if fulfilled) or reason (if rejected).
  7. Promise.race(promises) – waits for the first promise to settle, and its result/error becomes the outcome.
  8. Promise.any(promises) (recently added method) – waits for the first promise to fulfill, and its result becomes the outcome. If all of the given promises are rejected, AggregateError becomes the error of Promise.any.
  9. Promise.resolve(value) – makes a resolved promise with the given value.
  10. Promise.reject(error) – makes a rejected promise with the given error.
  11. In most Javascript engines, including browsers and Node.js, the concept of microtasks is closely tied with the “event loop” and “macrotasks”. As these have no direct relation to promises, they are covered in another part of the tutorial, in the article Event loop: microtasks and macrotasks.

Hope you liked the Blog if you liked it share it save it and follow me for such content. Thanks happy coding.