JavaScript Asynchronous Programming

Yasith Wimukthi
10 min readMar 15, 2022

--

When lines of code in any programming language are executed and completed in the order in which they occur, this is referred to as synchronous flow or behavior. If you consider the list below to be a collection of synchronously executing lines of code, it will always be executed in the same sequence. Line 2 will be executed and completed only after Line 1 has finished and before Line 3 begins:

print(“Line 1”)
print(“Line 2”)
print(“Line 3”)

The order in which the lines of code appear does not determine the order in which they finish execution in asynchronicity. Line 3 can be finished before Lines 1 and 2. Lines 1 and 2 can be processed concurrently, however Line 2 can complete before Line 1.

Asynchronous programming is a method of programming in which lines of code execute independently and in parallel to the main application thread and inform the main thread when they are done with a success or failure status.
Some operations may take longer to complete than others, therefore instead of having the entire execution wait for the operation to complete, they can execute asynchronously in the background and provide a response when they are finished. This encourages better utilization and reduces wait time.

JavaScript Runtime

JavaScript is a single-threaded synchronous execution language. A JavaScript engine is a program that interprets and executes JavaScript code. JavaScript has gone a long way since its inception, when it was executed using simple interpreters, to the engines available today, which are far more performant and effective.

The key components of the JS Runtime are as follows:

  1. The JavaScript Engine, which handles JS code execution, is made up of the following components:
  • Memory Heap: The section of the JS Engine that allocates memory for the variables and functions of the code being run.
  • Call Stack: The single-threaded stack in which each execution context stacks up as the code is run. The event loop is constantly examining the call stack, which is a Last-In-First-Out (LIFO) queue, to determine whether any functions need to be performed. It continues to add any function calls it detects while executing to the call stack and executes them in the sequence of LIFO.

2. The Web APIs are available in the browser but not in the JS Engine. Asynchronous actions like as the setTimeout() function, DOM manipulation techniques, XMLHttpRequest for AJAX, Geolocation, and LocalStorage are all part of the runtime environment.

3. Callback/Message Queue is a queue into which callbacks connected with asynchronous calls are placed once the corresponding asynchronous activity is completed. The event loop selects the callback for execution and pushes it for execution when the call stack is empty.

4. The Event Loop is the section that is always running and monitoring the Call Stack to see if there are any frames to be executed, which it then chooses to execute. Whether the call stack is empty, it looks in the Callback queue to determine if there are any callbacks that need to be processed. If one is available, it selects a callback from the message queue and places it in the Call Stack for execution.

Now consider how the JavaScript runtime, notably the Call Stack, is used during code execution for both synchronous and asynchronous code:

const secondFunction = () => console.log(‘calling second function’);const firstFunction = () => {      console.log(‘start first function’);      secondFunction;      console.log(‘first function end’);};firstFunction();

As detailed below, the preceding code is added to the Call Stack and executed by the Event Loop:

  • The firstFunction method is called, and the call stack is updated with the first execution context.
  • The initial console log is executed as the next statement on the Call Stack, is logged, and is then removed from the stack.
  • The following sentence calls another function, secondFunction(), which generates a new execution context for secondFunction.
  • The console log is added to the Call Stack, run, and removed from the stack in the first statement of secondFunction.
  • Because it is the end of secondFunction, the context for secondFunction is removed from the stack.
  • Finally, the firstFunction’s last console statement is performed, and firstFunction is completed and removed from the Call Stack.

The console output looks like the following:

start first function
calling second function
first function end

Now, we make the same function behave asynchronously by causing a delay with setTimeout in the secondFunction as seen below:

const secondFunction = () => setTimeout(() => console.log(‘calling second function’), 1000);const firstFunction = () => {      console.log(‘start first function’);      secondFunction;      console.log(‘first function end’);};firstFunction();

The event loop from the call stack was used to execute synchronous code. Other aspects of the JS runtime, such as the WebAPI and the message queue, are also engaged in asynchronous programming.

Let’s go over the steps below, with the first few stages remaining the same because there is no change in firstFunction:

  • The firstFunction method is called, and the call stack is updated with the first execution context.
  • The initial console log is executed as the next statement on the Call Stack, is logged, and is then removed from the stack.
  • The following sentence calls another function, secondFunction(), which generates a new execution context for secondFunction.
  • There is a setTimeout of 1 second (1000 ms) in secondFunction. The setTimeout JavaScript function is processed simultaneously by the browser. When the setTimeout() method is used, a new execution context is produced and added to the top of the stack. The timer is generated alongside the callback and sits on a different thread in the Web API, where it runs asynchronously for 1 second without interfering with the main code flow.
  • The setTimeout() method returns and the Call Stack is cleaned.
    The secondFunction() method is the same.
  • The firstFunction’s last console statement is performed, and firstFunction is completed and removed from the Call Stack.
  • After 1 seconds, the timer is removed from the Web API, and the related callback function is relocated to the Message Queue. It sits there until the execution stack is empty, at which point it is taken up by the Event Loop.
  • The Event Loop will keep an eye on the message queue and the Execution Stack at all times. When the execution stack is empty, it moves the callback from the Message Queue to it. As a result, a callback execution context is established, and the console log is executed, finished, and removed from the stack.

If you run the above code in your browser console, the output will look like this, with the last log appearing after a delay owing to the asynchronous setTimeout:

start first function
first function end
undefined
calling second function

Callbacks

Asynchronous programming guarantees that the program’s main thread is not blocked while waiting for a long-running procedure to finish. Asynchronous Callbacks are the most fundamental approach for dealing with asynchronous behavior.
A argument supplied to the asynchronous function is the callback function. The logic that will be executed when the asynchronous function is done is contained in the callback function:

const asyncResponse = callback => { 
setTimeout(() => callback (‘Response from asynchronous function’), 1000);
}

The asynchronous function in the above example is the asyncResponse function, which waits for 1 second before sending the response. It accepts as an argument a callback function, which is executed with the response:

asyncResponse (response => console.log(response))

As previously said, while calling the function, we must supply a function as a parameter, which in this case is defined as an anonymous function using the arrow syntax and includes the logic to log the answer received.
The following named function can be used to define the same thing:

function logAsyncResponse(response) {
console.log(response);
}
asyncResponse(logAsyncResponse);

The logging callback function is supplied as a parameter without the () since it is not being executed here, but a reference is passed to the main asynchronous function where it will be invoked. The technique (whether named or not) makes no difference, and the behavior is the same. The key concept is to send a callback function as a parameter, which is added to the message queue after the asynchronous function is done and is picked up by the event loop once the call stack is empty.

Promises

Promises are one of the most significant additions to JavaScript as part of ES6. Promises are constructs that, as compared to callbacks, manage asynchronous behavior considerably more cleanly.
A promise is defined by the ECMA Committee as follows:

“A Promise is an object that is used as a placeholder for the eventual results of a deferred (and possibly asynchronous) computation.”

A promise can be used to track a job that is expected to take some time and will finally provide a response. Promise is similar to making a true promise, which will result in a response whenever it is available.
The Promise constructor is used to generate a promise, as demonstrated below.

const myPromise = new Promise((resolve, reject) => {
if (Math.random() * 10 <= 5) {
resolve('Promise success!');
}
reject(new Error('Promise failed'));
});

A promise is associated with a time-consuming operation; it also changes state while waiting for a response and, eventually, when it gets the response.
A promise can be in any of the following states:

  • Pending: This is the state in which the promise is while it waits for the event to finish, hence it is said to be in the pending state.
  • Settled/Resolved: The promise is said to be settled or resolved once the asynchronous event has finished and the result has been received.
  • Fulfilled: It is considered to be fulfilled when the event is performed satisfactorily, the promise is fulfilled, and the reaction is accessible.
  • Rejected: If an error occurred throughout the procedure, the promise is said to be rejected.

The promise constructor, as seen in the myPromise example, accepts a callback as an argument. The callback function accepts two argument functions, which are used when the promise is eventually fulfilled:

  • resolve(value): This function is used to convey the promise’s success response. This will set the Promise Status to fulfilled. The value supplied as part of this function will be set as the promised value.
  • reject(error): This function is used to convey the promise’s error response. This will set the Promise Status to rejected. The error passed as a parameter to this function will be set to the promised value.

To consume a promise, we’d need two sets of handlers: one for the fulfilled state and one for the rejected state. The then() function is used to consume the promise.

There are a number different methods to achieve this syntactically, and we’ll go over each of them using the myPromise example.

  1. then() accepts two callbacks:
  • When the promise is resolved or settled, the first callback function is run.
  • When the promise is refused, the second callback function is called.
myPromise.then(success => {
console.log(success);
}, error => {
console.log(error);
});

2. The following two arguments are optional and can be handled selectively:

  • Only handle success:
myPromise.then(success => {
console.log(success);
});
  • Only handle error:
myPromise.then(null, error => {
console.log(error);
});

3. The chaining of catch to handle the error:

myPromise.then(success => {
console.log(success);
})
.catch(error => console.log(error));

Any promise may be managed by chaining the then and catch methods.
Furthermore, if many promises must be called, they may be chained using the then function an unlimited number of times and levels.

Async Await

With callbacks, Promises solved the problem, but the code remained large and convoluted. Async Await, a whole new asynchronous construct introduced in ES8 JavaScript, makes it easier to interact with promises. Both keywords, async and await, are put individually yet act in tandem to handle asynchronous behavior.

When the async keyword is used before a function, it indicates that the function will act asynchronously and will always return a promise, whether implicitly or explicitly.

const asyncFunc = async () => return "Async Function"asyncFunc().then((result => console.log(result) //Async Function

It can return a promise explicitly as shown:

const asyncFunc = async () => return Promise.resolve("Async Function")asyncFunc().then((result => console.log(result) //Async Function

It can also resolve into an error as shown:

const asyncFunc = async () => return Promise.reject(new Error('This promise is rejected with error!'));asyncFunc().catch(error => console.log(error));

Regardless of what is within an async function, it will either implicitly or explicitly return a promise.

Await

Within an async function, the await operator is used to wait for a promise. It is used before any asynchronous statement within an async function, and it stops the flow to await its completion. It elicits synchronous behavior from asynchronous behavior. An await statement’s answer is always a promise.

Now, as shown below, we will build an asynchronous function that returns an explicit promise.

const printLanguage = language => {
const promise= new Promise( (resolve) => {
setTimeout(() =>{
console.log(language);
resolve(language);
}, 2000);
}
);
return promise;
}

The promise function is the same as in the last example; the only change is how we consume it. Because it contains asynchronous statements, the function is designated as async.
As you can see from the three await instructions, it calls the asynchronous method printLanguage two times.

The following code demonstrates how async and await operate together:

async function getLanguage(){
const firstLanguage = await printLanguage("JavaScript");
const secondLanguage = await printLanguage("java");
return secondLanguage;
}
getLanguage().then((response)=>{
console.log(response);
});

After the first promise is met, the execution will go to the following line to acquire the second promise, and so on until the third promise is fulfilled. This is a sequential behavior imposed by the usage of await within the async method. If we don’t want the three calls to be done in order since there is no internal dependency, we can make them in parallel. We can achieve parallel processing of the asynchronous calls by making use of Promise.all as shown as follows:

async function getLanguage(){
const [firstLanguage, secondLanguage] = await Promise.all([printLanguage("JavaScript"),printLanguage("java")]);
return secondLanguage;
}
getLanguage().then((response)=>{
console.log(response);
});

This code appears to be simpler and more elegant than several callback loops and then catch chains. It also allows us to execute the statements synchronously or asynchronously within the async function by using await and Promise. all .

--

--

Yasith Wimukthi

Software Engineer at IFS |Full Stack Engineer| Software Engineering Fresh Graduate