Asynchronous Programming
Slide navigation: Forward with space bar, →, or PgDn. Backwards with ← or PgUp.
- Dynamic image loading:
function addImage(url) {
const request = new XMLHttpRequest()
request.open('GET', url)
request.responseType = 'blob'
request.addEventListener('load', () => {
const response = request.response
const blob = new Blob([response], {type: 'image/png'})
const img = document.createElement('img')
img.src = window.URL.createObjectURL(blob)
document.getElementById('images').appendChild(img)
})
request.send()
}
- XMLHttpRequest doesn't block.
- Asynchronous callback when request complete.
- What if you add multiple images?
addImage('hanafuda/1-1.png')
addImage('hanafuda/1-2.png')
addImage('hanafuda/1-3.png')
addImage('hanafuda/1-4.png')
- Can come in any order.
Demo
images.html
- JavaScript is single-threaded.
- Each function call runs to completion.
- That is good—no race conditions.
- Can't block in any function.
- Anything after a blocking activity must be done in callback.
- Doing tasks in order requires callbacks inside callbacks.
- Each callback may require error handling.
- Callback hell.

Promises
function loadImage(url) {
return new Promise((resolve, reject) => {
const callback = () => {
try {
const response = request.response
const blob = new Blob([response], {type: 'image/png'})
const img = document.createElement('img')
img.src = window.URL.createObjectURL(blob)
resolve(img)
} catch (e) {
reject(e)
}
}
const request = new XMLHttpRequest()
request.open('GET', url)
request.responseType = 'blob'
request.addEventListener('load', callback)
request.send()
})
}
function produceAfterDelay(what, when) {
return new Promise((resolve, reject) => {
const callback = () => resolve(what)
setTimeout(callback, when)
})
}
- When the constructor is called, it invokes its argument (the “executor function”).
- The executor function returns, then the constructor returns.
- The promise is in the pending state.
- When the async task has completed, it will invoke a callback that you provided in the executor function.
- If you call
reject
in a callback, the promise is rejected.
- If you call
resolve
with an object other than a promise, then the promise is resolved.
- In either of these two cases, the promise is settled.
- If you call
resolve
with another promise, then this promise stays pending until the other promise is resolved or rejected.
- Caution: Be sure to always call
resolve
or reject
in your callbacks, or the promise never exits the pending state.
- In particular, make sure your callbacks throw no exceptions.
- If an exception is thrown in the executor function, the promise will be rejected.
- As with all rejections, this happens some time after the constructor returns.
- You cannot obtain the result of a promise synchronously.
- Provide instructions what to do when the promise is settled.
- Use the
then
method to specify action after the promise is resolved.
const promise1 = loadImage('hanafuda/1-1.png')
promise1.then(img => {
document.getElementById('images').appendChild(img)
})
const promise2 = produceAfterDelay(42, 1000)
promise2.then(console.log)
then
is the only way to get a result out of a promise.
Working with Promises
Demo
images2.html
Demo
function produceAfterDelay(what, when) {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(what), when)
})
}
const promise2 = produceAfterDelay(42, 3000)
promise2.then(console.log)
- Implement
addImage
to return a promise that loads and adds the image to the right place. - Addd multiple images:
addImage('hanafuda/1-1.png').then(
() => addImage('hanafuda/1-2.png'))
- Caution: Not
addImage('hanafuda/1-1.png').then(
addImage('hanafuda/1-2.png'))
- Make more symmetric:
Promise.resolve()
.then(() => addImage('hanafuda/1-1.png'))
.then(() => addImage('hanafuda/1-2.png'))
- Can intermingle synchronous and async tasks:
Promise.resolve()
.then(() => loadImage('hanafuda/1-1.png'))
.then(img => append img)
.then(() => loadImage('hanafuda/1-2.png'))
.then(img => append img)
- Looping over multiple async tasks:
let p = Promise.resolve()
for (let i = 1; i <= n; i++) {
p = p.then(() => loadImage(`hanafuda/1-${i}.png`))
.then(img => append img)
}
Demo
images3.html
Fetch API
fetch('https://aws.random.cat/meow')
.then(result => result.json()) // Asynchronous
.then(console.log)
// An object such as { file: 'https://purr.objects-us-east-1.dream.io/i/pBo0r.jpg'}
promise.finally(onFinally)
calls onFinally
after the promise is resolved or rejected.
- No argument is passed—you don't know how the promise settled.
- Intended for cleanup
- Does not change the result/rejection reason of the promise.
Promise.all(iterable)
yields a promise that is resolved when all promises in the iterable are resolved.
- In that case, the combined promise is resolved with an iterable of all promise results.
- If the iterable contained non-promises, they are included in the result iterable.
- If any of the promises rejects, so does
Promise.all
.
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 contains non-promises, then the first one will be the result.
- If the iterable is empty, then
Promise.race
never settles.
Demo all and race with images
images5.html
Async and Await
- Functions:
async function(url) { ... }
- Arrow functions:
async url => { ... }
async (url, params) => { ... }
- Object literal methods:
{
async loadImage(url) { ... },
...
}
- Class methods:
class ImageLoader {
async load(url) { ... }
}
- In all cases, result is an
AsyncFunction
instance, not a Function
typeof
still reports 'function'
- Multiple
await
are ok:
async function putImages(url1, url2) {
const img1 = await loadImage(url1)
document.getElementById('images').appendChild(img1)
const img2 = await loadImage(url2)
document.getElementById('images').appendChild(img2)
}
- Loops are ok too:
async function putImages(urls) {
for (url of urls) {
const img = await loadImage(url)
document.getElementById('images').appendChild(img)
}
}
- Successive calls to
await
are done one after another:
const img1 = await loadImage(url1)
const img2 = await loadImage(url2) // Only starts after the first image was loaded
- This is probably not want you want. Instead, use
Promise.all
:
const [img1, img2] = await Promise.all([loadImage(url1), loadImage(url2)])
- But not
const [img1, img2] = [await loadImage(url1), await loadImage(url2)]
// Still sequential
- An
async
function always returns a promise.
async function getCatImageURL() {
const result = await fetch('https://aws.random.cat/meow')
const randomCat = await result.json()
// A string such as 'https://purr.objects-us-east-1.dream.io/i/mDh7a.jpg'
return randomCat.file
}
- Call to get a promise for a string:
getCatImageURL().then(console.log)
- Can also get result with
await
(in another async
function):
const url = await getCatImageURL()
- An
async
function doesn't have to call await
:
async function hello(who) {
return `Hello ${who}!`
}
hello('World').then(console.log)
- Throwing an exception in an
async
function yields a rejected promise.
async function getAnimalImageURL(type) {
if (type === 'cat') return await getProperty('https://aws.random.cat/meow', 'file')
else if (type === 'dog') return await getProperty('https://dog.ceo/api/breeds/image/random', 'message')
else throw Error('bad type') // Rejected promise
}
- Conversely, rejected promise from
await
turns into exception:
async function getAnimalImage(type) {
try {
const url = await getAnimalImageURL(type)
return loadImage(url)
} catch {
return BROKEN_IMAGE
}
}
Async Iterators and Generators
Async Iterators and Generators
The for await of
Loop
- Consume async iterable like this:
for await (img of loadImages(urls)) {
document.getElementById('images').appendChild(img)
}
for await of
works with any iterable of promises.
- Must be inside
async
function.
- Rejected promises are turned into exceptions.
- Cannot use async iterators in
for of
loop, spread, destructuring.
- Typical use case: paged data
async function* getPagedData(url) {
boolean done = false
const headers = { ... }
while (url !== undefined) {
const response = await fetch(url, headers)
yield response
url = nextURL(response) // Extract from response headers or body
}
}
async range
timedrange.js
End of Lesson 10