Async/Await explained via diagrams and examples

Introduction


The syntax async / await in JavaScript ES7 makes it easier to coordinate promises. If you have data to fetch from different databases or APIs, asynchronously and in a certain order, you may end up with spaghetti of promises and callbacks. The structure async / await makes it possible to succinctly describe this logic with a code that is more readable and easier to maintain.
This article explains JavaScript async/await syntax and semantics with simple diagrams and examples.
But first, let's start with a short overview of the promises. Feel free to skip this section if you are familiar with promises in JS.

In this section:

Promises

In JavaScript, a promise represents an abstraction of non-blocking asynchronous execution. JS promises are similar to Java Futures or C# task, if you have already encountered them.
Promises are typically used for network and I/O operations: for example, reading a file or making an HTTP call. Instead of blocking the thread current runtime, we create an asynchronous promise and we use the method then to attach a callback which will be activated when the pledge is complete. The callback can itself return a promise, and so we can chain promises efficiently.
To simplify all the examples, we will assume that the library request-promise has already been installed and loaded as:

const rp = require('request-promise');

Now we can query HTTP GET which returns a promise like:

const promise = rp('http://example.com/');

Next, let's look at this example:

console.log('Starting Execution'); const promise = rq('http://example.com/'); promise. then(result => console. log(result)); console.log("Can't know if promise has finished yet...");

We created a new promise on line 3, and we attached a callback on line 4. The promise is asynchronous so when we get to line 6 we can't tell if the promise is complete. If we run the code multiple times, the results may be different each time.
More generally, the code after any promise created runs concurrently with the promise.
It is not possible to block the operation sequence until the promise is completed. What differs from Java Future.get, which blocks the thread running until a Future is finished. In JavaScript, you can't easily expect a promise.
The following diagram describes the behavior of the sample.
Diagram of the 1st example
The process of a promise.
The only way to program code after a promise is to specify a callback via the method then.
La callback attached via then only executes after the promise has completed. If it fails (for example due to a network error) its callback will not be performed on me. To deal with failed promises, one can attach another callback via catch:

rp('http://example.com/'); .then(()=> console.log('Success')) .catch(e =>console.log(`Failed. ${e}`));

Finally, for the tests one can easily create “dummy” promises which end without/with error respectively via the methods Promise.resolve et Promise.reject.

const success = Promise. resolve('Resolved'); //Print "Successful result: Resolved" success .then(result => console.log(`Successful result: ${result}`)) .catch(e => console.log(`Failed with: ${e}` )); const fail = Promise. reject('Err'); //Printer "Failed with: Err" success .then(result => console.log(`Successful result: ${result}`)) .catch(e => console.log(`Failed with: ${e}` ));

For a more detailed tutorial about promises, I invite you to read this article

The problem – Composing promises

Using a single promise is quite simple. However, when one needs to code complex asynchronous logic, one can successfully combine several promises.
Write all the clauses then and asynchronous callbacks can quickly degenerate.
For example, imagine that we need to code a program that:

  1. Makes an HTTP call, waits for it to complete, and prints the result;
  2. And make two more HTTP calls in parallel;
  3. As soon as both calls complete, print their result.

The following piece of code shows one way to do this:

// Make the first call const call1promise = rp('http://example.com/'); call1promise.then( result => { // Done after first call completes console.log(result); const call2promise = rp('http://example.com/'); const call3promise = rp('http: //example.com/'); return Promise.all([call2promise,call3promise]); } ).then( arr =>{ // Done after both calls complete console.log(arr[0]); console.log(arr[1]); } );

We start by making the first HTTP call and we program a callback to process the result of the promise.
In the callback, we create two other promises for future HTTP requests.
These two promises are concurrent and require the establishment of a callback when both end. We must then combine them into a single promise via Promise.all, which creates a promise ending as soon as all concurrent promises are finished.
The result of the first callback is a promise, so we have to chain them again with a callback then which will display the results.
The following diagram shows the execution flow:
Diagram example 2
Flow of execution of the combination of promises.
For such a simple example, we end with two callback then and we must use Promise.all to synchronize competing promises.

But what if we had more asynchronous operations or if we had to add error handling?

With this approach, one can easily end up with spaghetti code consisting of then, Promise.all and callback.

Async function

An async function is a shorthand for defining a function that returns a promise.
For example, the following definitions are equivalent:

function f(){ return Promise. resolve('TEST'); } //asyncF is equivalent to f! async function asyncF(){ return 'TEST'; }

Similarly, async functions that throw exceptions are equivalent to functions that return rejected promises:

function f(){ return Promise. reject('Error'); } //asyncF is equivalent to f async function asyncF(){ return 'Error'; }

await

When we create a promise, we can only wait for it to complete asynchronously with a callback via then. Indeed, waiting for a promise is not possible to encourage the development of non-blocking code.
Otherwise, developers would be tempted to perform blocking operations because it's easier than working with promises and callback.
However, to synchronize promises we need to be able to wait. In other words, if an operation is asynchronous (ie wrapped in a promise) it should be able to wait for another asynchronous operation to complete.

But how could the JavaScript interpreter know if an operation is running in a promise or not?

The answer lies in the term async. The JavaScript interpreter knows that all operations in functions async will be encapsulated in promises and will be asynchronous. Therefore, it is possible to allow other promises to wait for completion.
Let's find out now await : It can only be used with functions async, and allows to synchronously wait for a promise. If we use a promise outside a function async we must use the callback then.

async function f(){ // response will evaluate as the resolved value of the promise const response = await rp('http://example.com/'); console.log(response); } // We can't use await outside of async function. // We need to use then callbacks .... f().then(() => console.log('Finished'));

Now back to the problem from the previous section:

// Encapsulate the solution in an async function async function solution() { // Wait for the first HTTP call and print the result console.log(await rp('http://example.com/')); // Spawn the HTTP calls without waiting for them - run them concurrently const call2promise = rp('http://example.com/'); // Doesn't wait! const call3promise = rp('http://example.com/'); // Doesn't wait! // After they are both spawn - wait for both of them const response2 = await call2promise; const response3 = await call3promise; console.log(response2); console.log(response3); } // Call the async function solution().then(() => console.log('Finished'));

In this code, we encapsulate the solution in a function async. This allows us to use await for promises, thus avoiding callback then. For after invoke the function async which creates a promise to encapsulate the logic to invoke other promises.
Indeed, in the first example (without async/await), the promises were executed in parallel and it will be the same here. Notice that we don't use the await that at line 11-12, we block execution here until both promises are completed.
After these lines we know that the promises are terminated as with the Promise.all(...).then(...) ) from the previous example.
Under the hood, await/async are translated with promises and callbacks then. Basically, it's just syntactic sugar for working with promises. Every time we use await, the interpreter creates a promise and encapsulates the rest of the operations of the async in a callback then.
Consider the following example:

async function f() { console.log('Starting F'); const result = await rp('http://example.com/'); console.log(result); }

The execution flow of f is described below. Like f is async, it runs “parallel” to the function that called it.
Diagram example 3
Function f executes and creates a promise. At this point the rest of the function is encapsulated in a callback, then executed at the end of the promise.

Error handling

In the majority of the examples presented, we assumed that the promise was successful and that therefore, the wait for a promise returned a value.
But be aware that if a promise is rejected in a function async and “expected” via await, it will throw an exception. It is then necessary to use the standard try/catch to handle these exceptions.

async function f() {
    try {
        const promesseResult = await Promise.reject('Error');
    } catch (e){
        console.log(e);
    }
}

If the function async does not handle the exception, which may be caused by a failed promise or some other bug, it will return a rejected promise.

async function f() { // Throws an exception const promiseResult = await Promise.reject('Error'); } // Printera "Error" f() .then(() => console.log('Success')) .catch(err => console.log(err)); async function g() { throw "Error"; } // Printera "Error" g() .then(() => console.log('Success')) .catch(err => console.log(err));

This allows us to process rejected promises through the standard error handling mechanism.

Discussion

Async/await is a language structure that complements promises, it allows us to work with less boilerplate. On the other hand, async/await does not replace promises.
For example, if we call a function async of a normal function or of the scope global, we will not be able to use the await and we will have to use the promises.

async function fAsync() { // return promise.resolve(5) return 5; } // We cannot use await fAsync(). Use then/catch fAsync() .then(r => console.log(`result is ${r}`));

Normally I try to encapsulate the majority of my asynchronous logic in one or more functions async, and I call them from my non-asynchronous code. Which minimizes the amount of then/catch callbacks to write.
The async/await pattern is syntactic sugar to be more concise with promises. Any code with the async/await pattern can be rewritten with simple promises. Ultimately, it's a matter of style.
Academics like to note that concurrency and parallelism are different. I let you watch the talk of Rob Pike on the subject.
Concurrency is the ability to compose independent processes (general definition of process) to work together. As for parallelism, it is being able to execute several processes simultaneously.
Concurrency focuses on the design and structure of the application while parallelism is a mode of execution.
For example, let's take a multi-threaded application. The separation of the application into several threads defines its competition model. The mapping of these processes in the available cores defines its level of parallelism.
A concurrent system can perform on a single processor and in this case it is not parallel.

Competition vs parallelism
Source: http://nikgrozev.com/2017/10/01/async-await/
With these definitions in mind, promises allow us to split our program into multiple concurrent models that may or may not run in parallel. Whether the execution of JavaScript is parallel or not depends on its implementation. For example, Node JS is single-threaded and if a promise is CPU bound, there won't be much parallelism. On the other hand, if we compile our code in Java Byte code via Nashorn for example, we are able in theory to map the promises CPU bound in different core and have parallelism. So I think promises, (either handwritten or async/await) represent a JavaScript application concurrency model.
source: Await and Async explained with Diagrams and examples, Nikolay Grozev
Translation by Yoan Ribeiro