Developers must be quick to become aware of the most recent revisions and feature enhancements to the JavaScript language in order to hold a competitive advantage and to capture ever-increasing performance from their websites. That is why ByteScout is here to provide you a summary of the most recent and most valuable additions to JavaScript’s functionality. In this article, we will explore developments listed in the ECMAScript 2017 language specification to discover how to implement these new features, and illustrate their relevance with useful examples.
ECMA released the 8th and most recent edition of its JavaScript language specification in June 2017. The 2016 edition represented a departure from the usual revision strategy from the ECMA group and picked up the pace. From that revision forward we will see more frequent revisions in this form (“ECMAScript 6” was actually renamed “ECMAScript 2015.”) A rich assortment of new functionality is described in this exhaustive specification, and in this article we will illuminate the best of these new technologies, with a particular view of asynchronous coding. And to bring absolute reality to the picture, we will explore how the new syntax and functionality are implemented in recent versions of the most widely used browsers.
We will look in detail at Async and Await functions, which dramatically improve asynchronous programming by providing support for writing Promise returning functions. Next, we will delve into the new memory consistency model introduced by Shared Memory and Atomics, which envisions implementation on parallel CPUs and enables communication between multi-agent programs using “atomics,” and which enforces a well-defined order of execution in resolving Promise returns. The emphasis in this summary is on the long-awaited arrival of true asynchronous functionality in JavaScript, at the levels of both concurrency and parallelism. We will illustrate how both are implemented in the new ECMA spec. Finally, because the ECMAScript spec is indeed vast, we will also provide several valuable reference points for continued exploration. Let’s dive right in with asynchronous implementation.
The 6th edition of ECMAScript specified the Promise as an object which represents the asynchronous state – pending, completion, or failure – of an asynchronous operation. Although ECMAScript 2017 specifies Async functions this does not deprecate Promise functionality, and in some cases, the Promise is more applicable than Async. So, let’s quickly cover Promises, because many developers have used this since ECMAScript 2015 adoption. Then we will move on to see how the Promise and Async are complimentary for a variety of operations.
A Promise is an object you attach callbacks to, rather than passing callbacks to a function. For instance, functions previously expected two callbacks, and then called one or the other depending on the state:
function1 successCallback1(result) { console.log("Succeeded - " + result); } function1 failureCallback1(error) { console.log("Failed - " + error); } doThis(successCallback1, failureCallback1);
Now, the more elegant method is to attach callbacks to the Promise returned by a function such as this:
let promise = doThis(); promise.then(successCallback1, failureCallback1); // or equally: doThis().then(successCallback1, failureCallback1);
Developers frequently need to run two or more asynchronous operations end-to-end with one operation starting when the previous one succeeds and using the result from the previous operation. To do this, we implement the Promise Chain, in which each subsequent operation starts when the previous operation succeeds, with the result from the previous step. We accomplish this by creating a Promise chain, where the .then returns a new Promise as in this simple example:
let promise = doThis(); let promise2 = promise.then(successCallback1, failureCallback1); // or even more simply: let promise2 = doThis().then(successCallback1, failureCallback1);
This is the asynchronous function call. In the above example, promise2 contains the state (completion, pending, failure) not only of doThis() but also of the passed successCallback1 or failureCallback1 functions, and these can also be asynchronous functions likewise returning a Promise. Then, callbacks added to promise2 queue after the Promise returned by successCallback1 or failureCallback1. Thus, each Promise is the completion state of another asynchronous operation in the Promise chain:
doThis().then(function1(result) { return doThat(result); }) .then(function1(newResult) { return doTheother(newResult); }) .then(function1(finalResult) { console.log('Final result: ' + finalResult); }) .catch(somefailureCallback1);
Always remember to return Promises up so that callbacks will chain correctly and errors will be caught. The Promise chain will stop with an exception and then look down the chain for catch handlers. Use .catch to chain Promises after failure as in this example:
doThis() .then(aresult => doThat(value)) .then(anewResult => doTheother(anewResult)) .then(afinalResult => console.log(`Final result: ${afinalResult}`)) .catch(afailureCallback);
What are some of the additional advantages of the Promise method? Callbacks are never executed prior to completion of the current run of the JavaScript event loop. We can add Callbacks with .then which can still be called as many times as needed, even after success or failure of an asynchronous operation. We can add multiple callbacks by calling .then as many times as needed, and these will be executed independently in order of insertion! This guarantees the intended order of execution based on the known order of callback insertions.
Async and Await are new JavaScript features introduced with ECMAScript 2017 specs which make asynchronous coding much easier and more straightforward. Async complements existing Promise objects and is fully compatible with existing APIs which use Promise objects. How do Async and Await enhance coding?
First, Async converts any function into a Promise. The syntax is explicit and easy to read. Promise chaining works as described above. And Async enables the use of Await. Together, these two methods give us the potential to write clean code that is clearly comprehensible. Placing Await before Promise forces the remaining code to wait until that particular Promise completes and returns its result. Await only works through Promises, not through callbacks, and only inside async functions.
In the following example, let’s compare two snippets, one using strictly Promises, and a second using Async. Here, we will fetch a JSON file from the server with a library function which sends an HTTP GET request to a URL. Because we have to wait a variable time for the HTTP request to resolve, this operation is naturally asynchronous.
In the first example, we manually create a Promise. Data from this request will become available in the .then block, resolve(json) returns the result:
function1 getJSON(){ return new Promise(function(resolve) { lib1.get('https://cdn.bytescout.com/example1.json') .then( function(json) { resolve(json); }); }); }
Compare with the Async approach below, in which the Async auto creates a Promise and returns it, so there is no need for the .then block.
async function1 getJSONAsync(){ let json = await lib1.get('https://bytescout/files/example1.json'); return json; } // ...and then call the function like this: getJSONAsync().then( function1(result) { // process the result. });
The result of this GET request is then available in the json variable, and we can return it as in any ordinary synchronous function. Apart from the syntax which is easier to read, the underlying functionality is the same, and so there is no compatibility between old and new methods. Here is an equivalent comparison of Promise and Async in which we will add a .catch block:
// first the Promise version: function1 fetchData(url) { return fetch(url) .then(request => request.text()) .then(text => { return JSON.parse(text); }) .catch(err => { console.log(`Error: ${err.stack}`); }); }
// compare with the Async version:
async function1 fetchData(url) { try { let request = await fetch(url); let text1 = await request.text(); return JSON.parse(text1); } catch (err) { console.log(`Error: ${err.stack}`); } }
Suppose we need to make multiple asynchronous calls whose results are not interdependent. As mentioned earlier, Async resolves calls in order, but this is often not ideal where calls resolve in variable time. Look at this Async code:
async function getAlphaBetaGamma() { let Alpha = await getValueAlpha(); // requires 4 seconds let Beta = await getValueBeta(); // requires 7 seconds let Gamma = await getValueGamma(); // requires 5 seconds return Alpha*Beta*Gamma; }
Each Await call waits for the result from the previous one, and this is great when there is a dependency among the results. However, in the case above, we’re only multiplying the results and the order in which we receive them does not matter to the outcome. In this case, we can save two seconds by sending all three requests at the same time. We can do this by using Promise.all(). Promise guarantees that we have all three results before continuing, but liberates us from the strict order of Await as in this code:
async function getABC() { // Promise.all() allows us to send all requests at the same time. let results = await Promise.all([ getValueAlpha, getValueBeta, getValueGamma ]); return results.reduce((total,value) => total * value); }
This reduces the total time to the slowest returned value. So, the great benefit of the availability of both Promise and Async is that we can choose one or the other; Async is ideal for the return of results when the order of execution is known, while Promise serves us better when results are not interdependent. In both cases, the resolution is guaranteed prior to the continuation!
The catch clause will handle errors provoked by the awaited asynchronous calls or any other failing code we may have written inside the try block.
Async functions also provide the .catch for handling errors, as in this example:
async function1 doThisAsync(){ let result = await anyAsyncCall(); return result; }
// …and catch errors when calling the function like this:
doThisAsync(). .then(successHandler1) .catch(errorHandler1);
Language enhancements like Async potentially offer developers one new solution and one new headache: the code is more elegant, but not all browsers support the new JavaScript syntax. Ultimately this means some end users will see a failure. According to Mozilla, Async and Await are supported fully by Firefox 55. Opera 42, Edge, Safari 10.1 and Chrome 55 likewise support the new ECMAScript standard. Internet Explorer does not support Async as of the time of this writing.
Continuing the theme of asynchronous processing, enter the new JavaScript memory model. We live in a world of concurrency, but JavaScript was not good at modeling the concurrent world until ECMAScript 2017. Now, with the SharedArrayBuffer objects, JavaScript is maturing toward modeling concurrency. In order to understand the new Memory Consistency Model defined by the ECMAScript 2017 standard, let’s first be clear about the distinction between concurrency and parallelism.
Let me give a quick example to define the difference between concurrency and parallelism. The input/output devices such as mouse and keyboard on the computer are conceptually operating together at the same time, although it may be that the computer has only one CPU that checks the state of each device one at a time, sequentially, and this is what we call concurrency. If the computer has only one processor and the mouse and the keyboard are threaded, then this is concurrent but it’s not parallel. On the other hand, two computations happening on separate cores at the same time, as coordinated parts of a single procedure is an example of parallelism. A vector dot product is a type of computation which is elegantly distributed for parallel processing. This definition leads us to another important new feature of ECMAScript 2017 which is called “shared memory.”
JavaScript programs are often threaded into separate processes called “workers,” shared processes which run in the background. Workers were introduced to solve the concurrency problem in JavaScript. Web Workers furthermore communicate with each other via messaging to accomplish parallelism. We call postMessage after creating a Worker to pass data. But this gets rather complicated, because of handler functions and the necessity of cloning. This tedious method is replaced in ECMAScript 2017 by shared memory.
Shared memory features enable workers to share memory buffers in order to communicate faster and more readily. Shared memory works with Atomics toward the end of implementing a new memory model in JavaScript which facilitates multiple agents to function concurrently, as part of one program. Atomics sequentially order their calls to shared memory. How do we share memory buffers between two threads in actual practice?
In the example below, we use the new data structure called SharedArrayBuffer. This shared memory buffer is the actual memory along with typed array for access which acts like a view. See the comments inside the snippet below. First, let’s create an array and share it with a worker. Four seconds later, we’ll write a change in the shared array. After eight seconds we’ll print the array in the worker to verify the change. Here is the code for main.js in our example:
// SharedArrayBuffer example // 1. Create a worker const worker1 = new Worker('worker1.js') const length = 10; // 2. Create the shared buffer here: const sharedBuffer1 = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * length) // 3. Create the typed array on top of 2. const sharedArray = new Int32Array(sharedBuffer) // 4. Create an array with 10 numbers for (i = 0; i < length; i++) sharedArray[i] = i && sharedArray[i - 1] + 1 // 5. Send shared memory to worker: worker1.postMessage(sharedBuffer) // 6. schedule write to array after 4 sec. setTimeout(function() { console.log('[MASTER1] Change triggered.') sharedArray[0] = 123 }, 4000)
And now put this code in the worker.js file:
self.addEventListener('message', (m) => { // Create Int32Array on the shared memory area const sharedArray = new Int32Array(m.data) console.log('[WORKER1] Received SharedArrayBuffer. First value: ' + sharedArray[0]) setTimeout(() => console.log('[WORKER1] First value now: ' + sharedArray[0]), 8000) });
NOTE that the worker cannot directly access data from the SharedArrayBuffer. That is achieved indirectly by use of the TypedArray. Worker1 receives the memory and creates a TypedArray based on that. Now the worker has access to that shared memory, and we can test it after eight seconds when the main thread will have written a change to the array. The output should read ‘123’ after a change.
If we would rather not wait a fixed amount of time to check for a change to the array in the above sample and would prefer instead to discover the change when it happens, then we can leverage the methods in Atomics. Atomics effectively preserve or guarantee the integrity of data contained in an array which is accessed by more than one thread. This is analogous to a lock, which determines which of multiple agents has access to an area of shared memory at a specified time, and coordinates the operations of the various agents upon shared resources.
Let’s go ahead and recode the previous example using atomic.load to detect a change to the TypedArray:
self.addEventListener('message', (m) => { const sharedArray = new Int32Array(m.data) console.log('[WORKER1] Received SharedArrayBuffer. First value : ' + sharedArray[0]) while (Atomics.load(sharedArray, 0) === 0); console.log('[WORKER1] Changed! New value : ' + sharedArray[0]) });
Here, the Atomics.load takes the TypedArray as its first argument and index as the second, and we see a dynamic result if a change is made to the array! Here is the long-awaited capacity for coding true parallelism in JavaScript.
Support for Atomics methods and properties as of this writing is a sketchy matter due to the novelty and complexity of implementation. Mozilla describes the various browsers’ support for Atomics methods individually per browser version. Adoption may require time, but it’s inevitable because of the ever increasing scope of the JavaScript language. The new optimizations specified in ECMAScript 2017 enable us to improve every aspect of the coding process, including performance and readability.
Now that JavaScript has migrated into servers via Node.js, and to desktop apps the language once limited to browsers is now capable of nearly any application. The new ECMAScript 2017 is a vast specification of enhancements to JavaScript. To continue the study of this important resource, please see the official ECMA specs in the links in the references at the end of this ByteScout presentation.
ECMAScript® 2017 Language Specification (ECMA-262, 8th edition, June 2017)
https://www.ecma-international.org/ecma-262/8.0/index.html
Standard ECMA-262, 6th Edition, June 2015, ECMAScript 2015 Language Specification:
https://www.ecma-international.org/ecma-262/6.0/
Sample code following the ECMAScript spec outline: