''' JavaScript asynchronous behavior: ES5, ES6, ES2016 (ES7), ES2017 (ES8) and TypeScript | Modern JS

JavaScript asynchronous behavior: ES5, ES6, ES2016 (ES7), ES2017 (ES8) and TypeScript

Asynchronous execution has always been one of JavaScript's biggest cruxes. The problem stems from the fact JavaScript engines have historically been single-threaded. This means that if you execute operations A, B & C on a single-thread, operation B can't start until operation A finishes and operation C can't start until operation B finishes. This behavior which is also called blocking or sequential, has both its benefits and drawbacks.

One the plus side, single-threaded greatly simplifies program design because you know beforehand no two tasks can run simultaneously (e.g. there's no danger a task can overwrite the work of another or that a subsequent task lacks data from a prior task that hasn't finished). One the negative side, single-threaded is detremintal for things like UI (User Interfaces) and I/O operations like reading/writing files (e.g. you don't want a UI to block/'freeze' while another task takes 5 or 10 seconds to finish, and you also don't want to stop everything while a task reads or writes a large file).

So throughout the years, JavaScript has attempted to alleviate the drawbacks of single threaded execution by adding a series of features to tackle asynchronous problems. Up next, you'll learn the techniques available in the different JavaScript flavors -- ES5, ES6, ES2016 (ES7), ES2017 (ES8) and TypeScript -- to deal with asynchronous execution.

ES5: Asynchronous callbacks

Callbacks are a general purpose technique used across programming languages that allow the execution of code -- generally a function-- to be passed as an argument to other code -- also, generally a function -- with the expectation the latter code call back (execute) the former code at a given time. Callback functions are among the earliest and still widely used techniques that support asynchronous execution in JavaScript.

By the time ES5 became mainstream, callback functions were an established JavaScript development practice. ES5 itself added various new features that relied on the use of callback functions like Array.forEach[1], but the better known XMLHttpRequest[2] object data type -- which is technically not part of ECMAScript -- stormed the web and into every major browser as the foundation for AJAX (Asynchronous JavaScript and XML).

To start a concrete discussion on callback functions let's use AJAX, which is the technique you're most likely to have used in past JavaScript projects given its popularity. AJAX emerged as a means to get data into a browser from a remote server without the browser blocking. This means AJAX triggers a call to a remote server -- asynchronously -- while the main sequence can continue uninterrupted and callback functions are assigned the duties to wait out and process the remote server response. Listing 5-1 illustrates a simple AJAX sequence that uses jQuery library.

Listing 5-1. ES5: jQuery callback functions in AJAX method call
// Operation 1 finishes here
// Operation 2 (AJAX) starts here 
$.ajax("/remoteservice/")
  .done(function(data) {
    console.log("success");
  })
  .fail(function() {
    console.log("error");
  })
  .always(function() {
    console.log("complete");
  });
// Operation 3 starts here, right away

Notice in listing 5-1 that after operation 1 finishes, operation 2 with $.ajax starts and makes a call to a remote server. Because operation 2 is an AJAX call, it doesn't wait for the remote server to answer and the execution continues immediately to operation 3. So who or what handles the remote server response ? You can see that after the initial statement in operation 2 $.ajax("/remoteservice/") there are three chained functions .done(), .fail() and .always(). These last functions are callbacks and handle the remote server response from the AJAX call. If the remote server returns a successful response .done is run, if the remote server sends a failed response .fail() is run, and irrespective of the remote server response .always() is always run.

An important characteristic of callback functions is there's no assurance at what time they get called -- it can be 5, 10, 15 seconds or 1 hour later -- it all depends on how long the parent function takes to execute -- in this case whatever time the /remoteservice/ URL takes to send a response -- the key thing is other method calls in the workflow don't have to wait for the parent function to complete.

But as simple as it's to integrate asynchronous behavior in JavaScript through a callback function, there's a major drawback with callback functions when you need to link multiple asynchronous tasks one afer the other. Listing 5-2 illustrates a series of nested AJAX calls -- based on the snippet from listing 5-1 -- to better illustrate the problem with callback functions.

Listing 5-2. ES5: jQuery nested callback functions in AJAX method call
// Operation 1 finishes here
// Operation 2 (AJAX) starts here 
$.ajax("/remoteservice/")
  .done(function(data) {
    console.log("success");
    $.ajax("/otherremoteservice/")
     .done(function(data) {
       console.log("nested success");   
       $.ajax("/yetanotherremoteservice/")
        .done(function(data) {
         console.log("nested nested success"); 
       });
   });
  });
// Operation 3 starts here, right away

As you can see in listing 5-2, once the first AJAX call completes successfully, it triggers yet another AJAX call, and once this last call completes successfully, it triggers yet another AJAX call. Notice this sequence doesn't even take into account error handling (i.e. the .fail() callback used in listing 5-1) or an always executed handler (i.e. the .always() callback used in listing 5-1). If an error happened on the first AJAX call, it would require an additional tree of calls, and to handle an error on the second AJAX call, yet another tree of calls. This type of callback nesting can quickly get out of hand, to the point it's often named callback hell[3], due to how difficult it can be to work with (e.g. understand, debug, change).

If you think this type of operation nesting isn't too common in JavaScript, think again. With the demands placed on JavaScript applications, these series of dependent operations can easily be something like : an authentication service, that once successful loads a user's profile, that once successful loads a user's pictures or some other variation like it. Faced with these demands and to avoid confusing callback nesting, ES6 introduce the Promise object data type.

ES6: Promises

Promises are used for deferred and asynchronous operations. The need for JavaScript promises had existed for some time, to the point various JavaScript libraries emerged to fill in this void (e.g. WinJS[4], RSVP.js[5], Bluebird[6]). But it was only until ES6 that JavaScript got a native first level object named Promise for this purpose -- just like String, Date and Object. With Promise becoming part of the core language, the use of promises in JavaScript only rose, making it important to understand why and how to use promises.

While callback functions can be considered a type of promise, since they promise to run after an event -- in the callback function example, a promise to process a remote server response -- promises avoid a lot of the baggage present in callback functions. Promise objects are passed around like any other object reference and are used to keep track of the state of a promise at any given time. The following example shows a function that checks if the input is a number and returns a Promise object as the result.

Listing 5-3. ES6: Promise object syntax
function promiseNumberCheck(argument) { 
     if(isNaN(argument)) {
       return Promise.reject(argument + " is not a number"); 
     } else { 
       return Promise.resolve("We got number " + argument);
     }
}
let test1 = promiseNumberCheck(1);
let test2 = promiseNumberCheck("nothing");

test1.then(function(result) {
	console.log(result);
}, function(error) { 
  console.log(error);
});

test2.then(function(result) {
	console.log(result);
}).catch(function(error) { 
   console.log(error);
});

The promiseNumberCheck function in listing 5-3 uses isNaN to determine if the argument is a number. If the argument is not a number the function returns a Promise object as rejected with the given reason, if the argument is a number the function returns a Promise object as resolved with the given number. Next, two calls are made to the promiseNumberCheck function -- once with a number, the other with a string -- and the Promise objects results are assigned to the test1 and test2 references.

Next, you can see the Promise objects have the then() method called on them. The purpose of the then() method is to analyze the status of a Promise object. In this case, the status of both Promise objects is clear and instant because we set them, but in other cases a Promise object's status may be neither clear nor instant (e.g. if a remote service is involved). So the purpose of the then() method is to say 'when the Promise object status is set (rejected or resolved) and whether it's instantly or in 5 minutes, then do this'.

The Promise object's then method has two variations. The first option in the test1 reference uses two functions, the first one to handle a resolved promise and the second to handle a rejected promise, in both cases the function's input is the value assigned to the promise. The second option used in the test2 reference is to chain the then() method with the catch() method, in this case the then() method handles the resolved promise with a single method and the catch() method is used to handle a rejected promise with its own method.

Now that you're familiar with the syntax and main methods in Promise objects, let's create another example in listing 5-4 that introduces a delay in Promise objects to better illustrate how they're useful.

Listing 5-4. ES6: Promise objects with delay
function promiseRandomSeconds() { 
     let randomSeconds =  Math.floor((Math.random() * 10) + 1);
     return new Promise(
           function(resolve, reject) {
               // Perform logic and then determine to resolve or reject
               window.setTimeout(
                  function() {
                    if (randomSeconds%2 === 0) { 
                      // Resolve the promise if its even
                      resolve(randomSeconds + " seconds");
                    } else { 
                      // Reject the promise if its odd
                      reject(randomSeconds + " seconds");
                    }
               },randomSeconds*1000);
           }
     );
}
let test1 = promiseRandomSeconds();
let test2 = promiseRandomSeconds();

test1.then(function(result) {
	console.log("Success: " +result);
}).catch(function(error) { 
   console.log("Fail: " + error);
});

test2.then(function(result) {
	console.log("Success: " + result);
}).catch(function(error) { 
   console.log("Fail: " + error);
});

console.log("Hey there, I didn't have to wait!");

The promiseRandomSeconds() function in listing 5-4 first generates a random number between 1 and 10 to use as a delay. Next, it immediately return a Promise instance with new Promise(). However, unlike the Promise objects used in the promiseNumberCheck() function in listing 5-3, the Promise object status in this example isn't determined until a later time, so the generic new Promise() syntax is used.

Notice that wrapped inside the new Promise() statement is the function(resolve, reject) { } function. It's inside this last function you can perform any required logic and then mark the Promise object with the resolve or reject references. In this case, the logic inside the function(resolve, reject) { } simply introduces a delay of promiseRandomSeconds -- the field value -- and depending on this value, marks the Promise object with resolve if the value is even or it marks the Promise object with reject if the value if odd.

Next in listing 5-4, two calls are made to the promiseRandomSeconds function and the results are assigned to the test1 and test2 references. Because the test1 and test2 references contain Promise object instances, the then() and except() methods are then used to determine the status of both Promise objects, just it was done in listing 5-3.

The most interesting part in listing 5-4 is the last log statement console.log("Hey there, I didn't have to wait!"). Even though the logic prior to this log statement introduces potential delays between 1 and 10 seconds, this last log statement is the first thing that's sent to output. Because the prior logic is based on Promise objects, it doesn't interfere with the main sequence and the final result (resolve or success) isn't reported until the Promise object determines it. In this case the Promise logic is pretty basic and based on a random number, but it can become a very powerful technique when used with tasks that can be delayed or return unexpected results (e.g. a REST service, reading a large file).

Finally, to close the discussion on the Promise object data types, listing 5-5 illustrated an example that chains Promise objects.

Listing 5-5. ES6: Chained Promise objects
function promiseWordAppender(word) { 
    return Promise.resolve(word);
}

let test1 = promiseWordAppender("It's a Bird...");

test1.then(function(result) {
            console.log(result);
            return result + "It's a plane...";
           }).then(function(result) { 
            console.log(result);
            return result + "No it's JavaScript Promises"
           }).then(function(result) { 
            console.log(result);
           });

To simplify things, the promiseWordAppender function in listing 5-5 always returns a Promise object as resolved with the given word argument. The interesting functionality in this example comes with the chained then() methods on the test1 reference. Notice that inside the first then() method a return statement is used, which is a way to invoke the parent method again. Inside the second then() method another return statement is used to call the parent method one more time. In this case the Promise logic simply appends words, but it's a powerful construct to be able to chain multiple then() statements with more complex logic (e.g. call service A, once finished call service B, once finished call service C), all this considering the times to execute each service can vary and are not guaranteed to be successful, not to mention it's far simpler to understand this syntax, than the one involving nested callbacks illustrated in listing 5-2.

Asynchronous ES7: Async functions

Asynchronous TypeScript

  1. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach?v=example#Syntax    

  2. https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest    

  3. http://callbackhell.com/    

  4. https://msdn.microsoft.com/en-us/library/windows/apps/br211867.aspx    

  5. https://github.com/tildeio/rsvp.js    

  6. http://bluebirdjs.com/