Topics in This Chapter
9.1 Concurrent Tasks in JavaScript
9.3 Immediately Settled Promises
9.7 Executing Multiple Promises
In this chapter, you will learn how to coordinate tasks that must be executed at some point in the future. We start with an in-depth look at the notion of promises. A promise is just what it sounds: an action that will produce a result at some point in the future, unless it dies with an exception. As you will see, promises can be executed in sequence or in parallel.
One drawback of promises is that you need to use method calls to combine them. The async
/await
constructs give you a much more pleasant syntax. You write code that uses regular control flow, and the compiler translates your code to a chain of promises.
Ideally, you could skip promises and move straight to the async
/await
syntax. However, I think it would be quite a challenge to understand the complexities and limitations of the syntax without knowing what it does behind your back.
We end the chapter with a discussion of asynchronous generators and iterators. All but the last section of this chapter should be required reading for intermediate JavaScript developers because asynchronous processing is ubiquitous in web applications.
A program is “concurrent” when it manages multiple activities with overlapping timelines. Concurrent programs in Java or C++ use multiple threads of execution. When a processor has more than one core, these threads truly run in parallel. But there is a problem—programmers must be careful to protect data, so that there is no corruption when a value is updated by different threads at the same time.
In contrast, a JavaScript program runs in a single thread. In particular, once a function starts, it will run to completion before any other part of your program starts running. That is good. You know that no other code will corrupt the data that your function uses. No other code will try to read any of the data until after the function returns. Inside your function, you can modify the program’s variables to your heart’s content, as long as you clean up before the function returns. You never have to worry about mut or deadlocks.
The problem with having a single thread is obvious: If a program needs to wait for something to happen—most commonly, for data across the Internet—it cannot do anything else. Therefore, time-consuming operations in JavaScript are always asynchronous. You specify what you want, and provide callback functions that are invoked when data is available or when an error has occurred. The current function continues execution so that other work can be done.
Let us look at a simple example: loading an image. The following function loads an image with a given URL and appends it to a given DOM element:
const addImage = (url, element) => { const request = new XMLHttpRequest() request.open('GET', url) request.responseType = 'blob' request.addEventListener('load', () => { if (request.status == 200) { const blob = new Blob([request.response], { type: 'image/png' }) const img = document.createElement('img') img.src = URL.createObjectURL(blob) element.appendChild(img) } else { console.log(`${request.status}: ${request.statusText}`) } }) request.addEventListener('error', event => console.log('Network error')); request.send() }
The details of the XMLHttpRequest
API are not important, except for one crucial fact. The image data are processed in a callback—the listener to the load
event.
If you call addImage
, the call returns immediately. The image is added to the DOM element much later, once the data is loaded.
Consider this example, where we load four images (taken from the Japanese Hanafuda card deck—see https://en.wikipedia.org/wiki/Hanafuda
):
const imgdiv = document.getElementById('images') addImage('hanafuda/1-1.png', imgdiv) addImage('hanafuda/1-2.png', imgdiv) addImage('hanafuda/1-3.png', imgdiv) addImage('hanafuda/1-4.png', imgdiv)
All four calls to addImage
return immediately. Whenever the data for an image arrive, a callback is invoked and the image is added. Note that you do not need to worry about corruption by concurrent callbacks. The callbacks are never intermingled. They run one after another in the single JavaScript thread. However, they can come in any order. If you load the web page with this program multiple times, the image order can change—see Figure 9-1.
All sample programs in this chapter are designed to be run in a web browser. The companion code has web pages that you can load into your browser and code snippets that you can paste into the development tools console.
To experiment with these files on your local system, you need to run a local web server. You can install light-server
with the command
npm install -g light-server
Change to the directory containing the files to serve and run the command
light-server -s .
Then point your browser to URLs such as http://localhost:4000/images.html
.
When loading images, it is fairly easy to cope with out-of-order arrival—see Exercise 1. But consider a more complex situation. Suppose you need to read remote data, and then, depending on the received data, read more data. For example, a web page might contain the URL of an image that you want to load.
In that case, you need to asynchronously read the web page, with a callback that scans the contents for the image URL. Then that image must be retrieved asynchronously, with another callback that adds the image to the desired location. Each retrieval requires error handling, which leads to more callbacks. With a few levels of processing, this programming style turns into “callback hell”—deeply nested callbacks with hard-to-understand success and failure paths.
In the following sections, you will learn how promises allow you to compose asynchronous tasks without nested callbacks.
A promise is an object that promises to produce a result eventually, hopefully. The result may not be available right away, and it might never be available if an error occurs.
That does not sound very promising, but as you will soon see, it is much easier to chain completion and error actions with promises than with callbacks.
In this section and the next, you will see how to make promises. This is a bit technical, and you rarely need to do it yourself. It is much more common to call library functions that return a promise. Feel free to gloss over these sections until you actually need to construct promises yourself.
A typical example for an API that produces promises is the Fetch API that all modern browsers support. The call
fetch('https://horstmann.com/javascript-impatient/hanafuda/index.html')
returns a promise that will yield the response from the HTTP request when it is available.
The Promise
constructor has a single argument, a function that has two arguments: handlers for success and failure outcomes. This function is called the “executor function.”
const myPromise = new Promise((resolve, reject) => {
// Body of the executor function
})
In the body of the executor function, you start the task that yields the desired result. Once the result is available, you pass it to the resolve
handler. Or, if you know that there won’t be a result, you invoke the reject
handler with the reason for failure. When work is completed asynchronously, these handlers will be invoked in some callback.
Here is an outline of the process:
const myPromise = new Promise((resolve, reject) => { const callback = (args) => { . . . if (success) resolve(result) else reject(reason) } invokeTask(callback) })
Let us put this to work in the simplest case: delivering a result after a delay. This function yields a promise to do that:
const produceAfterDelay = (result, delay) => { return new Promise((resolve, reject) => { const callback = () => resolve(result) setTimeout(callback, delay) }) }
In the executor function that is passed to the constructor, we call setTimeout
with a callback and the given delay. The callback will be invoked when the delay has passed. In the callback, we pass the result on to the resolve
handler. We don’t need to worry about errors, and the reject
handler is unused.
Here is a more complex function that yields a promise whose result is an image:
const loadImage = url => { return new Promise((resolve, reject) => { const request = new XMLHttpRequest() const callback = () => { if (request.status == 200) { const blob = new Blob([request.response], { type: 'image/png' }) const img = document.createElement('img') img.src = URL.createObjectURL(blob) resolve(img) } else { reject(Error(`${request.status}: ${request.statusText}`)) } } request.open('GET', url) request.responseType = 'blob' request.addEventListener('load', callback) request.addEventListener('error', event => reject(Error('Network error'))); request.send() }) }
The executor function configures an XMLHttpRequest
object and sends it. Upon receipt of the response, a callback produces an image and invokes the resolve
handler to pass it on. If an error occurs, it is passed to the reject
handler.
Let us look at the control flow of a promise in slow motion.
The Promise
constructor is called.
The executor function is called.
The executor function initiates an asynchronous task with one or more callbacks.
The executor function returns.
The constructor returns. The promise is now in the pending state.
The code invoking the constructor runs to completion.
The asynchronous task finishes.
A task callback is invoked.
That callback calls the resolve
or reject
handler, and the promise transitions to the fulfilled or rejected state. In either case, the promise is now settled.
There is one variation of the last step in the control flow. You can call resolve
with another promise. Then the current promise is resolved but not fulfilled. It stays pending until the subsequent promise is settled. For this reason, the handler function is called resolve
and not fulfill
.
Be sure to always call resolve
or reject
in your task callbacks, or the promise never exits the pending state.
That means that you have to pay attention to exceptions in task callbacks. If a task callback terminates with an exception instead of calling resolve
or reject
, then the promise cannot settle. In the loadImage
example, I carefully vetted the code to ensure that no exception was going to be thrown. In general, it is a good idea to use a try
/catch
statement in the callback and pass any exceptions to the reject
handler.
However, if an exception is thrown in the executor function, you don’t need to catch it. The constructor simply yields a rejected promise.
The call Promise.resolve(value)
makes a promise that is fulfilled immediately with the given value. This is useful in methods that returns promises, and where the answer is available right away in some cases:
const loadImage = url => { if (url === undefined) return Promise.resolve(brokenImage) . . . }
If you have a value that might be a promise or a plain value, the result of Promise.resolve(value)
definitely turns it into a promise. If the value is already a promise, it is simply returned.
For compatibility with libraries that predate standard ECMAScript promises, the Promise.resolve
method provides special treatment for “thenable” objects—that is, objects with a then
method. The then
method is invoked with a resolve handler and a reject handler, and returns a promise that is settled when either of the two handlers is called—see Exercise 6.
The call Promise.reject(error)
yields a promise that is immediately rejected with the given error.
Use it when a promise-producing function fails:
const loadImage = url => { if (url === undefined) { return Promise.reject(Error('No URL')) } else { return new Promise(. . .) } }
Now that you know how to construct a promise, you will want to obtain its result. You do not wait for the promise to settle. Instead, you provide actions that process the result or error once the promise has settled. Those actions will execute at some point after the end of the function that has scheduled them.
Use the then
method to specify an action that should be carried out once the promise is resolved. The action is a function that consumes the result.
const promise1 = produceAfterDelay(42, 1000) promise1.then(console.log) // Log the value when ready const promise2 = loadImage('hanafuda/1-1.png') promise2.then(img => imgdiv.appendChild(img)) // Append the image when ready
The then
method is the only way to get a result out of a promise.
You will see in Section 9.6, “Rejection Handling” (page 194), how to deal with rejected promises.
When you experiment with the loadImage
or fetch
function with different URLs, you will likely run into “cross-origin” errors. The JavaScript engine inside a browser will not allow JavaScript code to see results of web requests from third-party hosts unless those hosts agree that the access is safe and set a response header. Unfortunately, few sites have gone through the trouble. You can fetch the URLs at https://horstmann.com/javascript-impatient
or (as I write this) https://developer.mozilla.org
and https://aws.random.cat/meow
. If you want to experiment with other sites, you can use a CORS proxy or a browser plugin to overcome the browser check.
In the preceding section, you saw how to obtain the result of a promise. Now we tackle a more interesting case, where the promise result is passed to another asynchronous task.
If the action that you pass to then
yields another promise, the result is that other promise. To process its result, call the then
method once again.
Here is an example. We load an image, and then another:
const promise1 = loadImage('hanafuda/1-1.png')
const promise2 = promise1.then(img => {
imgdiv.appendChild(img)
return loadImage('hanafuda/1-2.png') // Another promise
})
promise2.then(img => {
imgdiv.appendChild(img)
})
There is no need to save each promise in a separate variable. Normally, one processes a chain of promises as a “pipeline.”
loadImage('hanafuda/1-1.png') .then(img => { imgdiv.appendChild(img) return loadImage('hanafuda/1-2.png') }) .then(img => imgdiv.appendChild(img))
With the Fetch API, you need to chain promises to read the contents of a web page:
fetch('https://developer.mozilla.org') .then(response => response.text()) .then(console.log)
The fetch
function returns a promise yielding the response, and the text
method yields another promise for the text content of the page.
You can intermingle synchronous and asynchronous tasks:
loadImage('hanafuda/1-1.png') .then(img => imgdiv.appendChild(img)) // Synchronous .then(() => loadImage('hanafuda/1-2.png')) // Asynchronous .then(img => imgdiv.appendChild(img)) // Synchronous
Technically, if a then
action yields a value that isn’t a promise, the then
method returns an immediately fulfilled promise. This allows further chaining with another then
method.
You can make promise pipelines more symmetric by starting out with an immediately fulfilled promise:
Promise.resolve() .then(() => loadImage('hanafuda/1-1.png')) .then(img => imgdiv.appendChild(img)) .then(() => loadImage('hanafuda/1-2.png')) .then(img => imgdiv.appendChild(img))
The preceding examples showed how to compose a fixed number of tasks. You can build an arbitrarily long pipeline of tasks with a loop:
let p = Promise.resolve() for (let i = 1; i <= n; i++) { p = p.then(() => loadImage(`hanafuda/1-${i}.png`)) .then(img => imgdiv.appendChild(img)) }
If the argument of the then
method is not a function, the argument is discarded! The following is wrong:
loadImage('hanafuda/1-1.png')
.then(img => imgdiv.appendChild(img))
.then(loadImage('hanafuda/1-2.png'))
// Error—argument of then
isn’t a function
.then(img => imgdiv.appendChild(img))
Here, then
is called with the return value of loadImage
—that is, a Promise
. If you call p.then(arg)
with an argument that is not a function, there is no error message. The argument is discarded, and the then
method returns a promise with the same result as p
. Also, note that the second call to loadImage
happens right after the first, without waiting for the first promise to settle.
In the preceding section, you saw how to carry out multiple asynchronous tasks in sequence. We focused on the “happy day” scenario when all of the tasks succeeded. Handling error paths can greatly complicate the program logic. Promises make it fairly easy to propagate errors through a pipeline of tasks.
You can supply a rejection handler when calling the then
method:
loadImage(url) .then( img => { // Promise has settled imgdiv.appendChild(img) }, reason => { // Promise was rejected console.log({reason}) imgdiv.appendChild(brokenImage) })
However, it is usually better to use the catch
method:
loadImage(url) .then( img => { // Promise has settled imgdiv.appendChild(img) }) .catch( reason => { // A prior promise was rejected console.log({reason}) imgdiv.appendChild(brokenImage) })
That way, errors in the resolve handler are also caught.
The catch
method yields a new promise based on the returned value, returned promise, or thrown exception of its handler argument.
If the handler returns without throwing an exception, then the resulting promise is resolved, and you can keep the pipeline going.
Often, a pipeline has a single rejection handler that is invoked when any of the tasks fails:
Promise.resolve() .then(() => loadImage('hanafuda/1-1.png')) .then(img => imgdiv.appendChild(img)) .then(() => loadImage('hanafuda/1-2.png')) .then(img => imgdiv.appendChild(img)) .catch(reason => console.log({reason}))
If a then
action throws an exception, the then
method yields a rejected promise. Chaining a rejected promise with another then
simply propagates that rejected promise. Therefore, the catch
handler at the end will handle a rejection at any stage of the pipeline.
The finally
method invokes a handler whether or not a promise has settled. The handler has no arguments since it is intended for cleanup, not for analyzing the promise result. The finally
method returns a promise with the same outcome as the one on which it was invoked, so that it can be included in a pipeline:
Promise.resolve() .then(() => loadImage('hanafuda/1-1.png')) .then(img => imgdiv.appendChild(img)) .finally(() => { doCleanup(. . .) }) .catch(reason => console.log({reason}))
When you have multiple promises and you want them all resolved, you can place them into an array or any iterable, and call Promise.all(iterable)
. You then obtain a promise that is resolved when all promises in the iterable are resolved. The value of the combined promise is an iterable of all promise results, in the same order as the promises themselves.
This gives us an easy way to load a sequence of images and append them in order:
const promises = [ loadImage('hanafuda/1-1.png'), loadImage('hanafuda/1-2.png'), loadImage('hanafuda/1-3.png'), loadImage('hanafuda/1-4.png')] Promise.all(promises) .then(images => { for (const img of images) imgdiv.appendChild(img) })
The Promise.all
does not actually run tasks in parallel. All tasks are executed sequentially in a single thread. However, the order in which they are scheduled is not predictable. For example, in the image loading example, you don’t know which image data arrives first.
As already mentioned, Promise.all
returns a promise for an iterable. That iterable contains the results of the individual promises in the correct order, regardless of the order in which they were obtained.
In the preceding sample code, the then
method is invoked when all images have been loaded, and they are appended from the images
iterable in the correct order.
If the iterable that you pass to Promise.all
contains non-promises, they are simply included in the result iterable.
If any of the promises is rejected, then Promise.all
yields a rejected promise whose error is that of the first rejected promise.
If you need more fine-grained control over rejections, use the Promise.allSettled
method instead. It yields a promise for an iterable whose elements are objects of the form
{ status: 'fulfilled', value: result }
or
{ status: 'rejected', reason: exception }
Exercise 8 shows how to process the results.
Sometimes, you want to carry out tasks in parallel, but you want to stop as soon as the first one has completed. A typical example is a search where you are satisfied with the first result. The Promise.race(iterable)
runs the promises in the iterable until one of them settles. That promise determines the outcome of the race.
If the iterable has non-promises, then one of them will be the result of the race. If the iterable is empty, then Promise.race(iterable)
never settles.
It is possible that a rejected promise wins the race. In that case, all other promises are abandoned, even though one of them might produce a result. A more useful method, Promise.any
, is currently a stage 3 candidate.
The Promise.any
method continues until one of the tasks has resolved. In the unhappy case that all promises are rejected, the resulting promise is rejected with an AggregateError
that collects all reasons for rejection.
Promise.any(promises) .then(result => . . .) // Process the result of the first settled promise .catch(error => . . .) // None of the promises settled
You have just seen how to build pipelines of promises with the then
and catch
methods, and how to execute a sequence of promises concurrently with Promise.all
and Promise.any
. However, this programming style is not very convenient. Instead of using familiar statement sequences and control flow, you need to set up a pipeline with method calls.
The await
/async
syntax makes working with promises much more natural.
The expression
let value = await promise
waits for the promise to settle and yields its value.
But wait. . .didn’t we learn at the beginning of this chapter that it is a terrible idea to keep waiting in a JavaScript function? Indeed it is, and you cannot use await
in a normal function. The await
operator can only occur in a function that is tagged with the async
keyword:
const putImage = async (url, element) => { const img = await loadImage(url) element.appendChild(img) }
The compiler transforms the code of an async
function so that any steps that occur after an await
operator are executed when the promise resolves. For example, the putImage
function is equivalent to:
const putImage = (url, element) => { loadImage(url) .then(img => element.appendChild(img)) }
Multiple await
are OK:
const putTwoImages = async (url1, url2, element) => { const img1 = await loadImage(url1) element.appendChild(img1) const img2 = await loadImage(url2) element.appendChild(img2) }
Loops are OK too:
const putImages = async (urls, element) => { for (url of urls) { const img = await loadImage(url) element.appendChild(img) } }
As you can see from these examples, the rewriting that the compiler does behind the scenes is not trivial.
If you forget the await
keyword when calling an async
function, the function is called and returns a promise, but the promise just sits there and does nothing. Consider this scenario, adapted from one of many confused blogs:
const putImages = async (urls, element) => {
for (url of urls)
putImage(url, element) // Error—no await
for async putImage
}
This function produces and forgets a number of Promise
objects, then returns a Promise.resolve(undefined)
. If all goes well, the images will be appended in some order. But if an exception occurs, nobody will catch it.
You can apply the async
keyword to the following:
Arrow functions:
async url => { . . . } async (url, params) => { . . . }
Methods:
class ImageLoader { async load(url) { . . . } }
Named and anonymous functions:
async function loadImage(url) { . . . } async function(url) { . . . }
Object literal methods:
obj = { async loadImage(url) { . . . }, . . . }
In all cases, the resulting function is an AsyncFunction
instance, not a Function
, even though typeof
still reports 'function'
.
An async
function looks as if it returned a value, but it always returns a promise. Here is an example. The URL https://aws.random.cat/meow
serves up locations of random cat pictures, returning a JSON object such as { file: 'https://purr.objects-us-east-1.dream.io/i/mDh7a.jpg' }
.
Using the Fetch API, we can get a promise for the content like this:
const result = await fetch('https://aws.random.cat/meow') const imageJSON = await result.json()
The second await
is necessary because in the Fetch API, JSON processing is asynchronous—the call result.json()
yields another promise.
Now we are ready to write a function that returns the URL of the cat image:
const getCatImageURL = async () => { const result = await fetch('https://aws.random.cat/meow') const imageJSON = await result.json() return imageJSON.file }
Of course, the function must be tagged as async
because it uses the await
operator.
The function appears to return a string. The point of the await
operator is to let you work with values, not promises. But that illusion ends when you leave an async
function. The value that appears in a return
statement always becomes a promise.
What can you do with an async
function? Since it returns a promise, you can harvest the result by calling then
:
getCatImageURL() .then(url => loadImage(url)) .then(img => imgdiv.appendChild(img))
Or you can get the result with the await
operator:
const url = await getCatImageURL() const img = await loadImage(url) imgdiv.appendChild(img)
The latter looks nicer, but it has to happen in another async
function. As you can see, once you are in the async
world, it is hard to leave.
Consider the last line in this async
function:
const loadCatImage = async () => { const result = await fetch('https://aws.random.cat/meow') const imageJSON = await result.json() return await loadImage(imageJSON.file) }
You can omit the last await
operator:
const loadCatImage = async () => { const result = await fetch('https://aws.random.cat/meow') const imageJSON = await result.json() return loadImage(imageJSON.file) }
Either way, this method returns a promise for the image that is asynchronously produced by the call to loadImage
.
I find the first version easier to understand since the async
/await
syntax consistently hides all promises.
Inside a try
/catch
statement, there is a subtle difference between return await promise
and return promise
—see Exercise 11. Here, you do not want to drop the await
operator.
If an async
function returns a value before ever having called await
, the value is wrapped into a resolved promise:
const getJSONProperty = async (url, key) => {
if (url === undefined) return null
// Actually returns Promise.resolve(null)
const result = await fetch(url)
const json = await result.json()
return json[key]
}
The async
functions of this section return a single value in the future. In Chapter 12, you will see how an async
iterator produces a sequence of values in the future. Here is an example that yields a range of integers, with a given delay between them.
async function* range(start, end, delay) { for (let current = start; current < end; current++) { yield await produceAfterDelay(current, delay) } }
Don’t worry about the syntax of this “async generator function.” You are unlikely to implement one, but you might use one that is provided by a library. You can harvest the results with a for await of
loop:
for await (const value of range(0, 10, 1000)) { console.log(value) }
This loop must be inside an async
function since it awaits all values.
Successive calls to await
are done one after another:
const img1 = await loadImage(url)
const img2 = await loadCatImage() // Only starts after the first image was loaded
It would be more efficient to load the images concurrently. Then you need to use Promise.all
:
const [img1, img2] = await Promise.all([loadImage(url), loadCatImage()])
To make sense of this expression, it is not sufficient to understand the async
/await
syntax. You really need to know about promises.
The argument of Promise.all
is an iterable of promises. Here, the loadImage
function is a regular function that returns a promise, and loadCatImage
is an async
function that implicitly returns a promise.
The Promise.all
method returns a promise, so we can call await
on it. The result of the promise is an array that we destructure.
If you don’t understand what goes on under the hood, it is easy to make mistakes. Consider this statement:
const [img1, img2] = Promise.all([await loadImage(url), await loadCatImage()])
// Error—still sequential
The statement compiles and runs. But it does not load the images concurrently. The call await loadImage(url)
must complete before the call await loadCatImage()
is initiated.
Throwing an exception in an async
function yields a rejected promise.
const getAnimalImageURL = async type => {
if (type === 'cat') {
return getJSONProperty('https://aws.random.cat/meow', 'file')
} else if (type === 'dog') {
return getJSONProperty('https://dog.ceo/api/breeds/image/random', 'message')
} else {
throw Error('bad type') // Async function returns rejected promise
}
}
Conversely, when the await
operator receives a rejected promise, it throws an exception. The following function catches the exception from the await
operator:
const getAnimalImage = async type => { try { const url = await getAnimalImageURL(type) return loadImage(url) } catch { return brokenImage } }
You do not have to surround every await
with a try
/catch
statement, but you need some strategy for error handling with async
functions. Perhaps your top-level async
function catches all asynchronous exceptions, or you document the fact that its callers must call catch
on the returned promise.
When a promise is rejected at the top level in Node.js, a stern warning occurs, stating that future versions of Node.js may instead terminate the process—see Exercise 12.
The sample program in Section 9.1, “Concurrent Tasks in JavaScript” (page 185), may not load the images in the correct order. How can you modify it without using futures so that the images are always appended in the correct order, no matter when they arrive?
Implement a function invokeAfterDelay
that yields a promise, invoking a given function after a given delay. Demonstrate by yielding a promise for a random number between 0 and 1. Print the result on the console when it is available.
Invoke the produceRandomAfterDelay
function from the preceding exercise twice and print the sum once the summands are available.
Write a loop that invokes the produceRandomAfterDelay
function from the preceding exercises n
times and prints the sum once the summands are available.
Provide a function addImage(url, element)
that is similar to that in Section 9.1, “Concurrent Tasks in JavaScript” (page 185). Return a promise so that one can chain the calls:
addImage('hanafuda/1-1.png') .then(() => addImage('hanafuda/1-2.png', imgdiv)) .then(() => addImage('hanafuda/1-3.png', imgdiv)) .then(() => addImage('hanafuda/1-4.png', imgdiv))
Then use the tip in Section 9.5, “Promise Chaining” (page 192), to make the chaining more symmetrical.
Demonstrate that the Promise.resolve
method turns any object with a then
method into a Promise
. Supply an object whose then
method randomly calls the resolve or reject handler.
Often, a client-side application needs to defer work until after the browser has finished loading the DOM. You can place such work into a listener for the DOMContentLoaded
event. But if document.readyState != 'loading'
, the loading has already happened, and the event won’t fire again. Capture both cases with a function yielding a promise, so that one can call
whenDOMContentLoaded().then(. . .)
Make an array of image URLs, some good, and some failing because of CORS (see the note at the end of Section 9.2, “Making Promises,” page 188). Turn each into a promise:
const urls = [. . .] const promises = urls.map(loadImage)
Call allSettled
on the array of promises. When that promise resolves, traverse the array, append the loaded images into a DOM element, and log those that failed:
Promise.allSettled(promises) .then(results => { for (result of results) if (result.status === 'fulfilled') . . . else . . . })
Repeat the preceding exercise, but use await
instead of then
.
Implement a function sleep
that yields a promise so that one can call
await sleep(1000)
Describe the difference between
const loadCatImage = async () => { try { const result = await fetch('https://aws.random.cat/meow') const imageJSON = await result.json() return loadImage(imageJSON.file) } catch { return brokenImage } }
and
const loadCatImage = async () => { try { const result = await fetch('https://aws.random.cat/meow') const imageJSON = await result.json() return await loadImage(imageJSON.file) } catch { return brokenImage } }
Hint: What happens if the future returned by loadImage
is rejected?
Experiment with calling an async
function that throws an exception in Node.js. Given
const rejectAfterDelay = (result, delay) => { return new Promise((resolve, reject) => { const callback = () => reject(result) setTimeout(callback, delay) }) }
try
const errorAfterDelay = async (message, delay) => await rejectAfterDelay(new Error(message), delay)
Now invoke the errorAfterDelay
function. What happens? How can you avoid this situation?
Explain how the error message from the preceding exercise can be useful for locating a forgotten await
operator, such as
const errorAfterDelay = async (message, delay) => { try { return rejectAfterDelay(new Error(message), 1000) } catch(e) { console.error(e) } }
Write complete programs that demonstrate the Promise.all
and Promise.race
functions of Section 9.7, “Executing Multiple Promises” (page 196).
Write a function produceAfterRandomDelay
that produces a value after a random delay between 0 and a given maximum milliseconds. Then produce an array of futures where the function is applied to 1, 2, . . . , 10, and pass it to Promise.all
. In which order will the results be collected?
Use the Fetch API to load a (CORS-friendly) image. Fetch the URL, then call blob()
on the response to get a promise for the BLOB. Turn it into an image as in the loadImage
function. Provide two implementations, one using then
and one using await
.
Use the Fetch API to obtain the HTML of a (CORS-friendly) web page. Search all image URLs and load each image.
When work is scheduled for the future, it may happen that due to changing circumstances the work is no longer needed and it should be canceled. Design a scheme for cancellation. Consider a multistep process, such as in the preceding exercise. At each stage, you will want to be able to abort the process. There is no standard way yet of doing this in JavaScript, but typically, APIs provide “cancellation tokens.” A fetchImages
function might receive an additional argument
const token = new CancellationToken() const images = fetchImages(url, token)
The caller can later decide to call
token.cancel()
In the implementation of an cancelable async
function, the call
token.throwIfCancellationRequested()
throws an exception if cancellation was indeed requested. Implement this mechanism and demonstrate it with an example.
Consider this code that carries out some asynchronous work such as fetching remote data, handles the data, and returns the promise for further processing:
const doAsyncWorkAndThen = handler => { const promise = asyncWork(); promise.then(result => handler(result)); return promise; }
What happens if handler
throws an exception? How should this code be reorganized?
What happens when you add async
to a function that doesn’t return promises?
What happens if you apply the await
operator to an expression that isn’t a promise? What happens if the expression throws an exception? Is there any reason why you would want to do this?