Submitted by gwagner on
In programming, we often have the situation that, when calling a possibly time-consuming input/output (I/O) operation (or any long-running operation, e.g., for performing a complex computation), the program execution has to wait for its result being returned before it can go on. Calling such an operation and waiting for its result, while the main program's further execution (and its entire thread) is blocked, represents a synchronous operation call. The implied waiting/blocking poses a problem for a JS program that is being executed in a browser thread since during the waiting time the user interface (in a browser tab) would be frozen, which is not acceptable from a usability point of view and therefore not accepted by browsers.
Consequently, in JavaScript, it is not possible to call an I/O operation,
e.g., for fetching data from a webpage (with the built-in
XMLHttpRequest or fetch API) or for accessing a
remote database (via HTTP request-response messaging) synchronously. These
types of operations have to be performed in an asynchronous
(non-blocking) manner, instead.
Asynchronous programming concepts in JavaScript have undergone an evolution
from callbacks to promises to generators
(coroutines) and, most recently, to asynchronous procedure calls
with await procedure invocation expressions and asynchronous
procedure definitions with async. Each evolution step has made
asynchronous programming a little bit easier for those who have taken the
effort to get familiar with it.
Due to this evolution, operations of older JS input/output APIs available
in the form of built-in objects, like XMLHttpRequest for HTTP
messaging or indexedDB for object database management, work
with callbacks, while newer APIs, like fetch for HTTP
messaging, work with promises and can also be invoked with
await.
Callbacks
A simple asynchronous programming approach consists of defining a procedure that is to be executed as soon as the asynchronous operation completes. This allows to continue the program execution after the invocation of the asynchronous operation, however, without assuming that the operation result is available. But how does the execution environment know, which procedure to call after completing the asynchronous operation?
In JS, we can pass a JS function as an argument in the invocation of the asynchronous operation. A callback is such a JS function.
Consider the following example. An external JS file can be dynamically
loaded (in the context of an already loaded webpage with associated JS
code) by (1) programmatically creating an HTML script element
DOM object with the file's URL as the value of the script's
src attribute, and (2) inserting the newly created
script element after the last child node of the document's
head element:
function loadJsFile( fileURL) { const scriptEl = document.createElement("script"); script.src = fileURL; document.head.append( scriptEl); }
When the new script element is inserted into the document's DOM, e.g.,
with the help of the asynchronous DOM operation append (at
the end of the loadJsFile procedure), the browser will load
the JS file and then parse and execute it, which will take some time.
Let's assume that we have a JS code file containing the definition of a
function addTwoNumbers that does what its name says and we
first load the file and then invoke the function in the following way:
loadJsFile("addTwoNumbers.js"); console.log( addTwoNumbers( 1, 2));
This wouldn't work. We would get an error message instead of the sum of 1
and 2, since the intended result of the first statement, the availability
of the addTwoNumbers function, is not (yet) obtained when the
second statement is executed.
We can fix this by adding a callback procedure as a second parameter to
the loadJsFile procedure and assign it as an event handler of
the JS file load event :
function loadJsFile( fileURL, callback) { const scriptEl = document.createElement("script"); script.src = fileURL; script.onload = callback; document.head.append( scriptEl); }
Now when calling loadJsFile we can provide the code to be
executed after loading the "addTwoNumbers.js" file in an anonymous
callback function:
loadJsFile("addTwoNumbers.js", function () { console.log( addTwoNumbers( 1, 2)); // results in 3 ]);
Since the loading of the JS file can fail, we should better add some
error handling for this case by defining an event handler for the
error event. We can handle possible errors within the
callback procedure by calling it with an error argument:
function loadJsFile( fileURL, callback) { const scriptEl = document.createElement("script"); script.src = fileURL; script.onload = callback; script.onerror = function () { callback( new Error(`Script load error for ${fileURL}`)); }; document.head.append( scriptEl); }
Now we call loadJsFile with an anonymous callback function
having an error parameter:
loadJsFile("addTwoNumbers.js", function (error) { if (!error) console.log( addTwoNumbers(1,2)); // results in 3 else console.log( error); ]);
Callbacks work well as an asynchronous programming approach in simple cases. But when it is necessary to perform several asynchronous operations in a sequence, one quickly ends up in a "callback hell", a term that refers to the resulting deeply nested code structures that are hard to read and maintain.
Promises
A promise (also called future in some programming
languages, like in Python) is a special object that provides the deferred
result of an asynchronous operation to the code that waits for this
result. A promise object is initially in the state pending. If
the asynchronous operation succeeds (in the case when the
resolve function is called with an argument providing the
result value), the promise state is changed from pending to
fulfilled. If it fails (in the case when the reject
function is called with an argument providing the error), the promise
state is changed from pending to rejected.
An example of a built-in asynchronous operation that returns a promise is
import for dynamically loading JS code files (and ES6
modules). We can use it instead of the user-defined
loadJsFile procedure discussed in the previous section for
loading the addTwoNumbers.js file and subsequently executing
code that uses the addTwoNumbers function (or reporting an
error if the loading failed):
import("addTwoNumbers.js") .then( function () { console.log( addTwoNumbers( 1, 2)); }) .catch( function (error) { console.log( error); });
This example code shows that on the promise object returned by
import we can call the predefined functions then
and catch:
then- for continuing the execution only when the
importoperation is completed with a fulfilled promise, and catch- for processing the error result of a rejected promise.
The general approach of asynchronous programming with promises
requires each asynchronous operation to return a promise object that
typically provides either a result value, when the promise is fulfilled,
or an error value, when the promise is rejected. For user-defined
asynchronous procedures, this means that they have to create a promise as
their return value, as shown in the promise-valued loadJsFile
function presented below.
A promise object can be created with the help of the Promise
constructor by providing an anonymous function expression as the argument
of the Promise constructor invocation (with two parameters
resolve and reject representing JS functions).
We do this in the following example of a promise-valued
loadJsFile function, which is a variant of the previously
discussed callback-based loadJsFile procedure:
function loadJsFile( fileURL) { return new Promise( function (resolve, reject) { const scriptEl = document.createElement("script"); scriptEl.src = fileURL; scriptEl.onload = resolve; scriptEl.onerror = function () { reject( new Error(`Script load error for ${fileURL}`)); }; document.head.append( scriptEl); }); }
This new version of the asynchronous loadJsFile operation is
used in the following way:
loadJsFile("addTwoNumbers.js") .then( function () { console.log( addTwoNumbers( 1, 2)); }) .catch( function (error) { console.log( error); });
We can see that even the syntax of a simple promise-valued function call
with then and catch is more clear than the
syntax of a callback-based asynchronous procedure call. This advantage is
even more significant when it comes to chaining asynchronous procedure
calls, as in the following example where we first sequentially load three
JS files and then invoke their functions:
loadJsFile("addTwoNumbers.js") .then( function () { return loadJsFile("multiplyBy3.js");}) .then( function () { return loadJsFile("decrementBy2.js");}) .then( function () { console.log( decrementBy2( multiplyBy3( addTwoNumbers(1,2))));}) .catch( function (error) { console.log( error); });
Notice that for executing a sequence of asynchronous operations with
then, we need to make sure that each
then-function returns a promise.
As an alternative to the sequential execution of asynchronous operations,
we may also execute them in parallel with
Promise.all:
Promise.all([ loadJsFile("addTwoNumbers.js"), loadJsFile("multiplyBy3.js"), loadJsFile("decrementBy2.js") ]) .then( function () { console.log( decrementBy2( multiplyBy3( addTwoNumbers(1,2)))); }) .catch( function (error) {console.log( error);});
Unlike loadJsFile, which simply completes with a side effect
(the loading of JS code), but without a result value being returned, a
typical asynchronous operation returns a promise object that provides
either a result value, when the promise is fulfilled, or an error value,
when the promise is rejected.
Let's consider another example, where we have asynchronous operations
with result values. The JS built-in fetch operation allows
retrieving the contents of a remote resource file via sending HTTP request
messages in two steps:
- In the first step, it returns a promise that resolves with a
responseobject as its result value containing the HTTP header information retrieved. - Then, invoking the
text()or thejson()function on the previously retrievedresponseobject returns a promise that resolves to the HTTP response message's body (in the form of a string or a JSON object) when it is retrieved from the remote server.
In such a case, when we chain two or more asynchronous operation calls with result values, each successor call can be expressed as a transformation from the previous result to a new result using arrow functions as shown in line 2 of the following example:
fetch("user1.json") .then( response => response.json()) .then( function (user1) {alert( user1.name);}) .catch( function (error) {console.log( error);});
Notice that the text file "user1.json" is assumed to contain a JSON object describing a particular user with a name field. This JSON object is retrieved with the arrow function expression in line 2.
Calling asynchronous operations with await
When a program with a statement containing an asynchronous procedure call
(with await) is executed, the program will run up to that
statement, call the procedure, and suspend execution until the
asynchronous procedure execution completes, which means that if it returns
a Promise, it is settled. That suspension of execution means that
control is returned to the event loop, such that other asynchronous
procedures also get a chance to run. If the Promise of the asynchronous
procedure execution is fulfilled, the execution of the program is resumed
and the value of the await expression is that of the
fulfilled Promise. If it is rejected, the await expression
throws the value of the rejected Promise (its error).
When we use await for invoking a Promise-valued JS function,
we typically do not use Promise chaining with .then, because
await handles the waiting for us. And we can use a regular
try-catch block instead of a Promise chaining
.catch clause, as shown in the following example code:
try { await loadJsFile("addTwoNumbers.js"); console.log( addTwoNumbers(2,3)); } catch (error) { console.log( error); }
Notice that this is the code of an ES6 module. In a normal JS file,
await can only be used within async functions.
When we call several asynchronous procedures in succession with
await, the code reads in a natural way, similar to the code
for calling synchronous procedures:
try { await loadJsFile("addTwoNumbers.js"); await loadJsFile("multiplyBy3.js"); await loadJsFile("decrementBy2.js"); console.log( decrementBy2( multiplyBy3( addTwoNumbers(2,3)))); } catch (error) { console.log( error); }
In an async function, we can invoke Promise-valued functions
in await expressions. Since an async function
returns a Promise, it can itself be invoked with await.
async function load3JsFiles() { await loadJsFile("addTwoNumbers.js"); await loadJsFile("multiplyBy3.js"); await loadJsFile("decrementBy2.js"); } try { await load3JsFiles(); console.log( decrementBy2( multiplyBy3( addTwoNumbers(2,3)))); } catch (error) { console.log( error); }
In the more typical case of asynchronous operation calls with result
values, we obtain code like the following await-based version of the above
promise-based example of using fetch:
try { const response = await fetch("user1.json"); const user1 = await response.json(); alert( user1.name); } catch (error) { console.log( error); }
For more about asynchronous programming techniques, see Promises, async/await and Demystifying Async Programming in Javascript.

