Without particular keywords and techniques, the JavaScript engine executes statements one after the next in the sequence of the written code. In most cases, this is necessary because the result of a line is used in the following line.

"use strict";

/* Line 1 */ const firstName = "Mahatma";
/* Line 2 */ const familyName = "Gandhi";
/* Line 3 */ const completeName = firstName + " " + familyName;

Lines 1 and 2 must be finished entirely before line 3 can be executed. This is the usual sequential behavior.

But there are situations where it is not necessary for the following statements to wait for the end of the current one. Or, you expect that an activity will run for a long time, and you want to do something else in the meanwhile. Such parallel execution has the potential to reduce the overall response time dramatically. That becomes possible because modern computers have multiple CPUs and are able to perform multiple tasks at the same time. In the above example, lines 1 and 2 may run in parallel. Moreover, the client/server architecture delegates activities across multiple servers.

Typical situations are long-running database updates, handling of huge files, or CPU-intensive computations. But even for simple-looking things like the rendering of an HTML page, a browser typically runs multiple tasks.

Single-threaded

edit

Natively, "JavaScript is single-threaded and all the JavaScript code executes in a single thread. This includes your program source code and third-party libraries that you include in your program. When a program makes an I/O operation to read a file or a network request, this blocks the main thread"[1].

To reach the goal of simultaneous activities anyway, browsers, service workers, libraries, and servers offer additional appropriate interfaces. Their primary use case is the unblocking execution of HTTP and database requests; a smaller proportion focuses on CPU-intensive computations.

At the level of the JavaScript language, there are three techniques to achieve asynchronism - what 'feels' like simultaneity.

  • Callback function
  • Promise
  • Keywords async and await

The basic technique is the use of callback functions. This term is used for functions that are passed as an argument to another function, especially - but not only - for functions that implement an asynchronous behavior.

A Promise represents the final completion or failure of an asynchronous operation, including its result. It steers further processing after the end of such asynchronous running operations.

Because the evaluation of Promises with .then and .catch may lead to hard-to-read code - especially if they are nested -, JavaScript offers the keywords async and await. Their usage generates well-arranged code comparable with try .. catch .. finally of traditional JavaScript. But they don't implement additional features. Instead, under the hood they are based on Promises.

Strictly sequential? No.

edit

To demonstrate that code is not always executed in strict sequential order, we use a script that contains a CPU-intensive computation within a more or less huge loop. Depending on your computer, it runs for several seconds.

"use strict";

// function with CPU-intensive computation
async function func_async(upper) {
  await null;                                           // (1)
  return new Promise(function(resolve, reject) {        // (2)
    console.log("Starting loop with upper limit: " + upper);
    if (upper < 0) {
      // an arbitrary test to generate a failure
      reject(upper + " is negative. Abort.");
    }
    for (let i = 0; i < upper; i++) {
       // an arbitrary math function for test purpose
       const s = Math.sin(i);                           // (3)
    }
    console.log("Finished loop for: " + upper);
    resolve("Computed: " + upper);
  })
}

const doTask = function(arr) {
  for (let i = 0; i < arr.length; i++) {
    console.log("Function invocation with number: " + arr[i]);
    func_async(array1[i])                              // (4)
      .then((msg) => console.log("Ok. " + msg))
      .catch((msg) => console.log("Error. " + msg));
    console.log("Behind invocation for number: " + arr[i]);
  }
}

const array1 = [3234567890, 10, -30];
doTask(array1);
console.log("End of program. Really?");

Expected output:

Function invocation with number: 3234567890
Behind invocation for number: 3234567890
Function invocation with number: 10
Behind invocation for number: 10
Function invocation with number: -30
Behind invocation for number: -30
End of program. Really?
Starting loop with upper limit: 3234567890
Finished loop for: 3234567890
Starting loop with upper limit: 10
Finished loop for: 10
Starting loop with upper limit: -30
Finished loop for: -30
Ok. Computed: 3234567890
Ok. Computed: 10
Error. -30 is negative. Abort.
  • The core of the asynchronous function func_async is a loop where a mathematical computation is done [(3) line 14]. The loop needs more or less time depending on the given parameter.
  • The return value of func_async is not a simple value but a Promise. [(2) line 6]
  • func_async is invoked by doTask once per element of the given array. [(4) line 24]
  • Because of the asynchronous nature of asyn_func the function doTask is executed totally before func_async runs! This can be observed by the program output.
  • The await null [(1) line 5] is a dummy call. It suspends the execution of func_async, giving doTask the chance to continue. If you delete this statement, the output will be different. Conclusion: To make the function really asynchronous you need both keywords, async in the function signature and await in the function body.
  • If you have a tool to observe your computer in detail, you can recognize that the three invocations of func_async doesn't run at the same time on different CPUs but run one after the next (on the same or on different CPUs).

Callback

edit

Passing a function as a parameter to an (asynchronous) function is the original technique in JavaScript. We demonstrate its purpose and advantage with the predefined setTimeout function. It takes two parameters. The first one is the callback function we are speaking about. The second is a number specifying the milliseconds after which the callback function is called.

"use strict";

function showMessageLater() {
  setTimeout(showMessage, 3000);  // in ms
}

function showMessage() {
  alert("Good morning.");
}

showMessage();       // immediate invocation
showMessageLater();  // invocation of 'showMessage' after 3 seconds

If showMessage is invocated, it runs instantly. If showMessageLater is invocated, it passes showMessage as a callback function to setTimeout, which executes it after a delay of 3 seconds.

Promise

edit

A Promise keeps track of whether an (asynchronous) function has been executed successfully or has terminated with an error, and it determines what happens next, invoking either .then or .catch.

The Promise is in one of three states:

  • Pending: Initial state, before 'resolved' or 'rejected'
  • Resolved: After successful completion of the function: resolve was called
  • Rejected: After completion of the function with an error: reject was called
"use strict";

// here, we have only the definition!
function demoPromise() {

  // The 'return' statement is executed immediately, and the calling program
  // continues its execution. But the value of 'return' keeps undefined until
  // either 'resolve' or 'reject' is executed here.
  return new Promise(function(resolve, reject) {
    //
    // perform some time-consuming actions like an 
    // access to a database
    //
    const result = true;  // for example

    if (result === true) {
      resolve("Demo worked.");
    } else {
      reject("Demo failed.");
    }
  })
}

demoPromise()  // invoke 'demoPromise'
  .then((msg) => console.log("Ok: " + msg))
  .catch((msg) => console.log("Error: " + msg));

console.log("End of script reached. But the program is still running.");

Expected output:

End of script reached. But the program is still running.
Ok: Demo worked.

When demoPromise is invoked, it creates a new Promise and returns it. Then, it performs the time-consuming action, and depending on the result, it invokes resolve or reject.

Next (in the meanwhile, the calling script has done other things), either the .then() or the .catch() functions behind the call to demoPromise are executed. The two functions accept an (anonymous) function as their parameter. The parameter given to the anonymous function is the value of the Promise.

 .then(   (     msg       )  =>  console.log("Ok: " + msg)   )
  |   | | |               |      |                       | | | |
  |   | | └── parameter ──┘      └────── funct. body ────┘ | | |
  |   | |                                                  | | |
  |   | └─────────────  anonymous function  ───────────────┘ | |
  |   |                                                      | |
  |   └───────  parameter of then() function  ───────────────┘ |
  |                                                            |
  └───────────────────  then() function  ──────────────────────┘

Please notice that the last statement of the script has been executed previously.

Many interfaces to libraries, APIs, and server functions are defined and implemented as functions returning a Promise, similar to the above demoPromise. As long as it's not necessary that you create your own asynchronous functions, it's not necessary that you create Promises. Often it's sufficient to call an external interface and work only with .then or .catch (or the async or await of next chapter).

async / await

edit

The keyword await forces the JavaScript engine to run the script named behind await entirely - including the new Promise part - before executing the following statement. Hence, - from the standpoint of the calling script - the asynchronous behavior is removed. Functions with await must be flagged in their signature with the keyword async.

"use strict";

// same as above
function demoPromise() {
  return new Promise(function(resolve, reject) {
    const result = true;  // for example
    if (result === true) {
      resolve("Demo worked.");
    } else {
      reject("Demo failed.");
    }
  })
}

// a function with the call to 'demoPromise'
// the keyword 'async' is necessary to allow 'await' inside
async function start() {
  try {
    // use 'await' to wait for the end of 'demoPromise'
    // before executing the following statement
    const msg =  await demoPromise();
    // without 'await', 'msg' contains the Promise, but without
    // the success- or error-message
    console.log("Ok: " + msg);
  } catch (msg) {
    console.log("Error: " + msg);
  }
}

start();
console.log("End of script reached. End of program?");

The use of async .. await allows you to work with the traditional try .. catch statement instead of .then and .catch.

A realistic example

edit

We use the freely available demo API https://jsonplaceholder.typicode.com/. It offers a small amount of test data in JSON format.

"use strict";

async function getUserData() {

  // fetch() is an asynchronous function of the Web API. It returns a Promise
  // see: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
  await fetch('https://jsonplaceholder.typicode.com/users')

    // for the use of 'response', see: https://developer.mozilla.org/en-US/docs/Web/API/Response/json
    // '.json()' reads the response stream
    .then((response) => { return response.json() })

    // in this case, 'users' is an array of objects (JSON format)
    .then((users) => {
      console.log(users); // total data: array with 10 elements

      // loop over the ten array elements
      for (const user of users) {
        console.log(user.name + " / " + user.email);
      }        
    })
    .catch((err) => console.log('Some error occurred: ' + err.message));
}

// same with 'try / catch'
async function getUserData_tc() {
  try {
    const response = await fetch('https://jsonplaceholder.typicode.com/users');
    const users = await response.json();
    console.log(users);
    console.log(users[0].name);
    for (const user of users) {
      console.log(user.name + " / " + user.email);
    }        
  } catch (err) {
    console.log('Some error occurred: ' + err.message);
  }
}

getUserData();
getUserData_tc();

The steps of the example are:

  • await fetch(): Get the data behind the URL. The await part guarantees that the script will not continue before the fetch has delivered all data.
  • json() reads the stream which contains the resulting data.
  • The resulting data is an array of 10 elements. Each element is in JSON format, e.g.:
{
  "id": 1,
  "name": "Leanne Graham",
  "username": "Bret",
  "email": "Sincere@april.biz",
  "address": {
    "street": "Kulas Light",
    "suite": "Apt. 556",
    "city": "Gwenborough",
    "zipcode": "92998-3874",
    "geo": {
      "lat": "-37.3159",
      "lng": "81.1496"
    }
  },
  "phone": "1-770-736-8031 x56442",
  "website": "hildegard.org",
  "company": {
    "name": "Romaguera-Crona",
    "catchPhrase": "Multi-layered client-server neural-net",
    "bs": "harness real-time e-markets"
  }
}
  • As an example, the script shows the complete data and some of its parts in the console.

Note: It's likely that you will run into a CORS error when you use an arbitrary URL.

Exercises

edit
... are available on another page (click here).

See also

edit

References

edit