Promise to Call me Back: Understanding Asynchronous JavaScript

Promise to Call me Back: Understanding Asynchronous JavaScript

JavaScript is single threaded which makes it inherently synchronous, This means code runs line by line on one core of the processor. However if you have functionality that needs to wait on something such as opening a file, a web response or other activity of this nature, then blocking the application until the operation is finished is a major point of failure in any application. The solution to prevent this blocking is provided by the Event Loop

THE EVENT LOOP

1_FA9NGxNB6-v1oI2qGEtlRQ.png

Our JavaScript engine has a Call Stack which keeps track of all the functions a script calls. The call stack stores all the function in a stack (an array-like data structure) and removes them once executed. This is the synchronous behavior of JavaScript.

Our JavaScript runtime provides something else for us, The Callback Queue. This stores asynchronous functions and operations. The Event Loop also provided by the runtime constantly runs to check when the Call Stack is empty. When it is, it pushes our asynchronous code to the Call stack for execution.

After execution, we would want to do things with data gotten from these async operations as all our synchronous code runs before async ones. JavaScript provides three mechanisms for doing that. These are:

  • Callback functions
  • Promise objects
  • Async functions

CALLBACKS

Callbacks are easy to understand so I won’t spend much time on it here. It is simply passing a function as an argument to another function (in this case our asynchronous function). And when the async function is done it calls the callback function with the callback taking the data returned from the async operation as it’s argument.

//example of a callback

window.addEventListener('load', (event) => {
  console.log('page is fully loaded');

// listens for the page to load and when it does, it runs the function taken as an argument
});

The major drawback of using callbacks is in a situation where we need to carry out series of async operations with the next operation dependent on data from the previous one. For example, using an app like twitter, you would fetch your account, then fetch accounts you follow, then fetch tweets from accounts you follow, then fetch comments on those tweets, then fetch replies, etc etc.

To do this with callbacks we end up nesting callbacks inside callbacks making our code very hard to read and Error handling becomes a nightmare.

1_VH2f5KmjKwlBaEYyOmSE2g.png

The alternative to this is Promises

PROMISES

A promise is a class based object, it is created using the new keyword and it’s constructor function. It contains a list of methods that gives our object superpowers.

A promise is defined by passing a callback function (called the executor function) as an argument to the Promise constructor. The executor usually takes two arguments: resolve and reject.

An asynchronous operation is defined inside the executor function and the intended result or error if any occurred is handled by the resolve and reject handler respectively.

const myPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('this is a promise');
  }, 300);
});

myPromise.then((value) => {
  console.log(value);
  // expected output: "this is a promise"
});

console.log(promise1);
// expected output: [object Promise]

Promise objects have a .then and a .catch methods which handle fulfilled results and errors if any occurred. They receive the outcome of the async operation as (data or error) from the resolve and reject handler in the promise constructor.

The .then method also returns a promise object. This means an async operation can be defined in it and the data returned can be handled just by chaining another .then method to it. This makes handling series of async operations extremely easy. A chain of .then methods can have a .catch method at the end which will catch any error in the queue making error handling also extremely easy.

doSomething()
  .then(function (result) {
    return doSomethingElse(result);
  })
  .then(function (newResult) {
    return doThirdThing(newResult);
  })
  .then(function (finalResult) {
    console.log(`Got the final result: ${finalResult}`);
  })
  .catch(failureCallback);

ASYNC/AWAIT

Lastly we have async functions. An async function is a function declared with the async keyword and the await keyword and the await keyword permitted within it. The purpose of async/await is to simplify the syntax necessary to write promise based APIs.

An async operation is defined inside the function and it (async function) returns a promise object which resolves the data gotten from the async operation.

Async functions can have zero or more await expressions

The await expressions make promise returning function behave as though they were synchronous by suspending execution until the promise returned is fulfilled or rejected.

const foo = async function () {
  await console.log(1);
}

// It is also equivalent to:

const foo = new Promise((resolve,reject) => resolve(1)).then((value)=> { console.log(value)}

The body of an async function can be thought of as being split by zero or more await expressions. Top-level code, up to and including the first await expression (if there is one), is run synchronously. In this way, an async function without an await expression will run synchronously. If there is an await expression inside the function body, however, the async function will always complete asynchronously.

Code after each await expression can be thought of as existing in a .thencallback. In this way a promise chain is progressively constructed with each reentrant step through the function. The return value forms the final link in the chain.

Thanks for reading. Please do more reading on these mechanisms to get extensive knowledge of them.