DEV Community

Aiman Jaffer
Aiman Jaffer

Posted on

JavaScript Asynchronous Patterns and Closures

It can be daunting to make the transition to frontend web development even for someone who has prior programming experience with strongly-typed or object-oriented languages such as Java. JavaScript has a plethora of quirks that make it a very unique language to say the least, but it is the predominant language of the internet and mastering it is crucial to success as a web developer. These were some of the JavaScript concepts that baffled me when I began (some still do) but I hope this post will help you gain a better understanding of some of these key concepts that you may encounter in the wild.

Asynchronous execution of code

JavaScript is a single-threaded language, this means that at any point during a program's execution there can be a maximum of one statement that is being executed which is followed by the next statement and so on. This works fine for statements where the bulk of work to be performed is handled by the CPU (aka CPU-intensive tasks). The problem occurs when a program involves code that performs I/O-intensive tasks (such as network calls, filesystem read/write operations etc.) and is followed by code that performs relatively quicker CPU-bound tasks that don't necessarily rely on the output of these I/O-intensive tasks but are forced to wait for them to finish before they can begin execution (due to the single-threaded nature of JavaScript). For example:

const fs = require('fs');  
const filepath = 'text.txt';
const data = fs.readFileSync(filepath, {encoding: 'utf8'});
let sum  = 3 + 5;
console.log(sum);
Enter fullscreen mode Exit fullscreen mode

In this example the statements involving calculating and logging the sum of 3 and 5 to the console must wait for the execution of all preceding code even though it is not dependent on the code that precedes it. This is an example of blocking I/O. This situation can be a significant bottleneck in the execution of a program and can lead to a unpleasant experience for the end-user of the program. Fortunately, there are a lot of ways to deal with this situation that are collectively known as asynchronous programming and when dealing with I/O operations specifically this is known as non-blocking I/O.
The 5 concepts we frequently encounter while implementing Asynchronous programming in JavaScript are:

  1. Callbacks
  2. Timeout functions
  3. Promises
  4. Async/Await
  5. Observables (This one is specific to RxJs)

Callbacks

To understand callbacks in JavaScript we must first be familiar with the underlying principle that is: functions are first-class citizens in JavaScript. This means that functions are just like any other JavaScript objects, in that they can be assigned to variables, passed as parameters to other functions and can be returned from other functions (Higher Order Functions). This feature of JavaScript is crucial in order to implement callbacks as we shall see in the following example:

//Synchronous Execution example:
function doSomethingWithDataSync (data) {
//...do some I/O intensive task which returns result
return result;
}

let result = doSomethingWithDataSync("Hello");
console.log(result);
let y = 3 + 5;
console.log(y);
Enter fullscreen mode Exit fullscreen mode

The same task can be performed using callbacks asynchronously as follows:

//Asynchronous Execution example:
function doSomethingWithDataAsync (data, callback){
//...do some I/O intensive task which returns result
if(error)
callback(error)
else
callback(null, result)
}

doSomethingWithDataAsync("Hello", function(error, data){
if(error)
console.log("Error occured");
else
console.log(data);
});
let y = 3 + 5;
console.log(y);
Enter fullscreen mode Exit fullscreen mode

In this example we pass a function that takes two arguments error, data as parameters to the function doSomethingWithDataAsync. Once the execution of the I/O intensive statement is completed, the callback function is called in one of two ways depending on whether an error occurred or the task executed successfully. In this example execution of statements let y = 3 + 5; and console.log(y); are not waiting for the execution of function doSomethingWithDataAsync and the callback function to complete. We'll now learn about how this callback is moved off the call stack in order to be processed at a later point in time.

Timeout functions

Functions such as setTimeout and setInterval are perhaps the oldest way of executing code asynchronously in JavaScript. The function setTimeout takes two parameters: the first is a callback function which contains some code that should be executed and the second is a minimum time (in milliseconds) to wait before the callback function is executed. Note that this is the minimum time and not a guarantee that the callback function will be executed immediately when this timer expires. To understand how this allows JavaScript to execute asynchronous code we must first familiarize ourselves with how the browser executes JavaScript via the Stack, CallbackQueue, Web APIs and the Event Loop.

setTimeout and setInterval belong to a category of functions that are collectively known as Web APIs. These functions are not part of the JavaScript language themselves but are APIs exposed by the browser in order to aid developers.

The Call Stack (or simple the Stack) is a LIFO (last-in first-out) data structure used by browers to determine the execution context of a particular piece of code. Whenever a function is called it is add to the top of the stack and when the function completes it is removed from the stack. Thus the function at the top of the stack is always the currently executing function.

The Event Queue is a data structure used by the browser to store functions that are waiting to be executed once the stack is empty.

The Event Loop is the browser construct that checks whether the stack is empty and moves the function in the front of the Queue to the Call Stack.

Now that we know what each of these individual pieces are, let's see how they work together in the following example:

console.log("Before setTimeout callback function");
setTimeout(()=>{
console.log("Inside setTimeout callback function");
},1000);
console.log("After setTimeout callback function");

Enter fullscreen mode Exit fullscreen mode

The output of this code snippet should be as follows:

Before setTimeout callback function
After setTimeout callback function
Inside setTimeout callback function
Enter fullscreen mode Exit fullscreen mode

With a gap of at least one second between when the second and third statements are displayed.

Let's take a look at the individual steps that allow this behavior to occur:
(We assume that before we begin both the Call Stack and Event Queue are empty)

  1. console.log("Before...") is the first statement that should be executed hence it is added to the stack. The message is displayed on the console and then the function is removed from the stack.
  2. setTimeout is called with a callback function and a minimum wait time of 1 second.
  3. setTimeout is added to the top of the stack and since it is a Web API it is immediately removed from the top of the stack.
  4. The browser registers the timer and the associated callback function and begins the timer.
  5. console.log("After...") is the next statement that should be executed hence it is added to the stack. The message is displayed on the console and then the function is removed from the stack.
  6. Once the timer expires after the specified duration of time, the callback function is added to the Event Queue.
  7. The Event Loop then checks whether the stack is empty and then moves the callback function (which is currently at the front of the Event Queue) to the stack for execution.
  8. The callback function executes, the message is logged to the console. 8.The callback function is removed from the stack.

Promises

One of the issues observed while programming utilizing callbacks is that code readability suffers, especially when dealing with nested callback functions. Promises offer an alternate syntax that significantly improves code readability through the use of operation chaining (as opposed to nesting). A Promise represents the eventual result of an asynchronous operation and it's associated value. At any given time, a promise can be in one of 3 states:

  1. Pending
  2. Fulfilled
  3. Rejected

We can deal with a promise that is in the fullfilled state via the .then(onFulfillment) method and perform error handling on a promise that is rejected via the .catch(onRejection) method. While chaining multiple promises all errors can be handled by a single .catch() placed at the end of the chain. An alternative to this is to specify both the onFulfillment and onRejection callbacks as arguments to .then() as .then(onFulfillment, onRejection). Internally a promise is fulfilled via the static method Promise.resolve(valueForSuccess) and rejected via the static method Promise.reject(valueForFailure).

Async/Await

Async/Await allows developers to write asynchronous code that very in style closely resembles synchronous code thus enhancing code readability even further than promise-style asynchronous code. Functions that contain asynchronous operations are marked with the async keyword and individual operations that are performed asynchronously are marked with the await keyword. Use of async await allows developers to use regular try catch blocks to perform error handling rather than .then() and .catch(). Also, Async functions are guaranteed to return Promises even if they are not explicitly created.

Observables

Observables are a technique for handling the execution of asynchronous tasks in the Angular framework through the use of RxJs library. Observables support multiple values as opposed to Promises that resolve to a single value. This pattern involves two actors. A Publisher that creates an Observable and provides a subscriber function. Any number of Consumers that call the .subscribe() method on the observable. The Consumer then receives new data via the Observable until the function completes execution or until they unsubscribe. The .subscribe() method takes three functions as parameters: next, error, complete. The first parameter is mandatory whereas the other two are optional. The next function is executed when the publisher publishes a new value, the error function is executed when the publisher sends an error notification and the complete function is executed when execution of the observable's subscriber function is complete.

Closures & Functions as First class Citizens

A closure in JavaScript is simply the combination of a function and the variables that it has access to when it was created. Let's understand this with and example:

function outerFunc(){
var playerName = "Michael Jordan";
function innerFunction(){
console.log("Player is: ", playerName);
} 
innerFunction();
}
outerFunc();
Enter fullscreen mode Exit fullscreen mode

The output of this code is Player is: Michael Jordan, pretty straightforward so far right? Now let's see what happens when we return the innerFunction from the outerFunction instead of calling it directly (We are allowed to do this because in JavaScript functions are objects). For Example:

function outerFunc(){
var playerName = "Michael Jordan";
function innerFunction(){
console.log("Player is: ", playerName);
} 
return innerFunction;
}
var getPlayerName = outerFunc();
getPlayerName();
Enter fullscreen mode Exit fullscreen mode

What do you expect will happen?

You might be inclined to think that since the inner function is now being called from a different context than that in which it was initially created inside it would not have access to the playerName variable. Go ahead try executing this code and see what happens for yourself.

You may be surprised to find that the output remains unchanged from the previous example. This is because functions in JavaScript are Closures, this means that functions once created always have access to the variables in the lexical scope in which they were defined.

Hope this was helpful !!
Links to useful resources below:

  1. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
  2. https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Introducing
  3. https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Async_await
  4. https://angular.io/guide/observables
  5. https://angular.io/guide/comparing-observables

Top comments (0)