Chapter 9. Asynchronous Programming

Images

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.

9.1 Concurrent Tasks in JavaScript

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.

Images Note

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.

Images

Figure 9-1    Images may load out of order

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.

9.2 Making Promises

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.

Images Note

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.

  1. The Promise constructor is called.

  2. The executor function is called.

  3. The executor function initiates an asynchronous task with one or more callbacks.

  4. The executor function returns.

  5. The constructor returns. The promise is now in the pending state.

  6. The code invoking the constructor runs to completion.

  7. The asynchronous task finishes.

  8. A task callback is invoked.

  9. 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.

Images Note

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.

9.3 Immediately Settled Promises

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.

Images Note

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(. . .)
  }
}

9.4 Obtaining Promise Results

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

Images Note

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.

Images Caution

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.

9.5 Promise Chaining

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))

Images Note

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.

Images Tip

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))
}

Images Caution

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.

9.6 Rejection Handling

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}))

9.7 Executing Multiple Promises

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.

9.8 Racing Multiple Promises

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.

Images Caution

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

9.9 Async Functions

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.

Images Caution

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:

Images Note

In all cases, the resulting function is an AsyncFunction instance, not a Function, even though typeof still reports 'function'.

9.10 Async Return Values

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.

Images Caution

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]
}

Images Note

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.

9.11 Concurrent Await

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.

9.12 Exceptions in Async Functions

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.

Exercises

  1. 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?

  2. 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.

  3. Invoke the produceRandomAfterDelay function from the preceding exercise twice and print the sum once the summands are available.

  4. Write a loop that invokes the produceRandomAfterDelay function from the preceding exercises n times and prints the sum once the summands are available.

  5. 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.

  6. 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.

  7. 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(. . .)
  8. 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 . . .
      })
  9. Repeat the preceding exercise, but use await instead of then.

  10. Implement a function sleep that yields a promise so that one can call

    await sleep(1000)
  11. 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?

  12. 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?

  13. 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) }
    }
  14. Write complete programs that demonstrate the Promise.all and Promise.race functions of Section 9.7, “Executing Multiple Promises” (page 196).

  15. 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?

  16. 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.

  17. Use the Fetch API to obtain the HTML of a (CORS-friendly) web page. Search all image URLs and load each image.

  18. 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.

  19. 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?

  20. What happens when you add async to a function that doesn’t return promises?

  21. 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?

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset