[ Previous Section | Appendix Index | Main Index ]

Section A.4

JavaScript Promises and Async Functions


This section introduces two new features in JavaScript: promises and async functions. These features are becoming increasingly common in JavaScript APIs. In particular, they are used in WebGPU, which is covered in Chapter 9. However, note that they are not used in any other part of this textbook.

A JavaScript promise represents a result that might be available at some time. If and when the result becomes available, the promise is fulfilled; it is said to "resolve." When that happens, the result can be returned, although in some cases the result is simply the knowledge that whatever the promise was waiting for has occurred. If something happens that means the promise cannot be fulfilled, then the promise is said to "reject." A programmer can provide functions to be called when the promise resolves or rejects.

Promises are a replacement for callback functions. A callback function is a function that is provided by a programmer to be called later, by the system, when something happens. For example in the standard JavaScript function

setTimeout( callbackFunction, timeToWait );

The callbackFunction will be called by the system timeToWait milliseconds after the setTimeout() function is executed. An important point is that setTimeout() returns immediately; it simply sets up the callback function to be called in the future. The same thing applies to promises: A program does not wait for a promise to resolve or reject; it simply arranges for something to happen later, when one of those things occurs.

Typical programmers are more likely to use promises created by some API than to create them directly. And they are more likely to use those promises with the await operator in async functions than to use them directly, so we will cover that case first.


A.4.1  Async Functions and await

The await operator is used to retrieve the result of a promise, when the promise has resolved. If, instead, the promise rejects, then the await operator will throw an exception. The syntax of an await expression is simply

await  promise

where promise is a promise. When the promise resolves, its result becomes the value of the expression. Here is an example from the WebGPU API (see Subsection 9.1.1):

let adapter = await navigator.gpu.requestAdapter();

The return value of navigator.gpu.requestAdapter() is a promise. When that promise resolves, the result of the promise becomes the value of the await expression, and that value is assigned to adapter.

An important thing to understand is that await does not actually stop and wait for the result—that is, it does not bring the JavaScript program to a halt while waiting. Instead, the function that contains the await expression is suspended until the result is available, while other parts of the program can continue to run.

The await operator can only be used inside an async function, that is, one whose definition is marked as async:

async function name( parameters ) {
     // await can be used here
}

When an async function is called, it is immediately executed up to the first occurrence of await in the function definition. At that point, the execution is suspended until the promise resolves or rejects. If it resolves, the execution resumes and continues until the next await, and so on. If at any point a promise rejects instead of resolving, an exception is thrown that can be caught and handled in the usual way. The function does not return until all of the promises in await expressions have resolved or until an exception causes the function to exit. Note that, necessarily, the function that called the async function is also suspended, even if that function is not async.

What this all amounts to is that await expressions are much like ordinary expressions and async functions are much like ordinary functions. They are written and used in the same way. This can make promises easier to use than callback functions, and this usage is one of their big advantages. However, the fact that async functions can be suspended introduces a new source of potential problems: You have to remember that other, unrelated things can happen in the middle of an async function.

Let's look at a specific example. The fetch API is an API for retrieving files from the Internet. (But without extra work, it can only fetch files from the same source as the web page on which it is used.) If url is the URL for some file, the function fetch(url) returns a promise that resolves when the file has been located or rejects when the file cannot be found. The expression await fetch(url) waits for the file to be located and returns the result. Curiously, the file has been located but not necessarily downloaded. If response is the object returned by await fetch(url), then the function response.text() returns another promise that resolves when the contents of the file are available. The value of await response.text() will be the file contents. A function to retrieve a text file and place its content in an element on the web page could be written like this:

async function loadTextFile( textFileURL ) {
   let response = await fetch(textFileURL);
   let text = await response.text();
   document.getElementById("textdisplay").innerHTML = text;
}

This will work, but might throw an exception, for example if access to the file is not allowed or if no such file exists. We might want to catch that exception. Furthermore, it can take some time to get the file, and other things can happen in the program while the function is waiting. In particular, the user might generate more events, maybe even an event that causes loadTextFile() to be called again, with a different URL! Now, there are two files being downloaded. Which one will appear on the web page? Which one should appear on the web page? This is the same sort of mess we can get into when doing parallel programming. (To be fair, we can get into a similar sort of mess when using callback functions, and there it can be even harder to untangle the mess.)

Let's say that a file download is triggered when the user clicks a certain button. One solution to the double-download mess would be to disable that button while a download is in progress, to prevent another download from being started. So, an improved version of our program might go something more like this:

async function loadTextFile( textFileURL ) {
    document.getElementById("downloadButton").disabled = true;
    document.getElementById("textdisplay").innerHTML = "Loading...";
    try {
       let response = await fetch(textFileURL);
       let text = await response.text();
       document.getElementById("textdisplay").innerHTML = text;
    }
    catch (e) {
       document.getElementById("textdisplay").innerHTML =
          "Can't fetch " + textFileURL + ".  Error: " + e;
    }
    finally {
       document.getElementById("downloadButton").disabled = false;
    }
}

The nice thing is that an async function looks essentially the same as a regular JavaScript function. The potential trap is that the flow of control in a program that uses async functions can be very different from the regular flow of control: Regular functions run from beginning to end with no interruption.


A.4.2  Using Promises Directly

The await operator makes promises fairly easy to use, but it is not always appropriate. A JavaScript promise is an object belonging to a class named Promise. There are methods in that class that make it possible to respond when a promise resolves or rejects. If somePromise is a promise, and onResolve is a function, then

somePromise.then( onResolve );

schedules onResolve to be called if and when the promise resolves. The parameter that is passed to onResolve will be the result of the promise. Note that we are essentially back to using callback functions: somePromise.then() returns immediately, and onResolve will be called, if at all, at some indeterminate future time. The parameter to then() is often an anonymous function. For example, assuming textPromise is a promise that eventually produces a string,

textPromise.then(
    str => alert("Hey, I just got " + str)
);

Now, technically, the return value of the onResolve callback in promise.then(onResolve) must be another promise. If not, the system will wrap the return value in a promise that immediately resolves to the same value. The promise that is returned by onResolve becomes the return value of the call to promise.then(). This means that you can chain another then() onto the return value from promise.then(). For example, let's rewrite our loadTextFile() example using then(). The basic version is:

function loadTextFileWithThen( textFileURL ) {
  fetch(textFileURL)
    .then( response => response.text() )
    .then( text => document.getElementById("textdisplay").innerHTML = text )
}

Here, fetch(textFileURL) returns a promise, and we can attach then() to that promise. When the anonymous function, response => response.text(), is called, the value of its parameter, response, is the result produced when fetch(textFileURL) resolves. The return value response.text() is a promise, and that promise becomes the return value from the first then(). The second then() is attached to that promise. When the callback function in the second then() is called, its parameter is the result produced by the result.text() promise.

Note that loadTextFileWithThen()is not an async function. It does not use await. When it is called, it returns immediately, without waiting for the text to arrive.

Now, you might wonder what happens if the promise rejects. The rejection causes an exception, but that exception is thrown at some indeterminate future time, when the promise rejects. Now, in fact, then() takes an optional second parameter that is a callback function, to be called if the promise rejects. However, you are more likely to respond to the rejection by using another method from the Promise class:

somePromise.catch( onReject )

The parameter, onReject, is a function that will be called if and when the promise rejects (or, when catch() is attached to a chain of calls to then(), when any of the promises in the chain rejects). The parameter to onReject will be the error message produced by the promise that rejects. (A catch() will also catch other kinds of exceptions that are generated by the promise.) And there is a finally() method in the Promise class that schedules a callback function to be called at the end of a then/catch chain. The callback function parameter in finally() takes no parameters. So, we might improve our text-loading example as follows:

function loadTextFileWithThen(textFileURL) {
  document.getElementById("downloadButton").disabled = true;
  fetch(textFileURL)
    .then( response => response.text() )
    .then( text => document.getElementById("textdisplay").innerHTML = text )
    .catch( e => document.getElementById("textdisplay").innerHTML =
                       "Can't fetch " + textFileURL + ".  Error: " + e )
    .finally( () => document.getElementById("downloadButton").disabled = false )
}

Generally, you should try to use async functions and await when possible. You should only occasionally have to use then() and catch(). And while you might find yourself using promise-based APIs, you will probably never need to create your own promise objects—a topic that is not covered in this textbook.


[ Previous Section | Appendix Index | Main Index ]