Promise Notes

Posted by Luna on April 3, 2022

At the very beginning, we use callback to handle asynchronous.

Then in ES6, promise is introduced.

The async and await were introduced in ES2017 and provide new syntax that simplifies asynchronous programming by allowing you to structure your promise-based code as if it’s synchronous.

Finally, in ES2018, asynchronous iterators and for/await loop were introduced.

Callbacks

A callback is a function that you write and then pass to some other function. The other function then invokes your function when some condition is met or some asynchronous event happens.

Below are some examples.

Timers

1
setTimeout(checkForUpdate, 10000)

The checkForUpdate function will be invoked 10000 milliseconds after the setTimeout() call.

Events

1
2
let root = document.getElementById('#root');
root.addEventListener('click', applyUpdate);

The applyUpdate is a callback function that will be invoked when click event occurs.

Network Events

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function getCurrentVersionNumber(versionCallback) {
    let request = new XMLHttpRequest();
    request.open("GET", "//some url");
    request.send();

    request.onload = function () {
        if (request.status === 200) {
            let currentVersion = parseFloat(request.responseText);
            verionCallback(null, currentVersion);
        } else {
            versionCallback(request.statusText, null);
        }
    }
}

versionCallback is a callback function that will be invoked with different params depends on different request response.

Promises

Now it’s time to have a look at the Promises.

A promise is an object that represents the reuslt of an asynchronous computation. The result may or may not be ready yet.

For example, if you are defining an asynchronous API like the getResponse(), and you want to make it Promise-based, instead of passing the callback argument, return a Promise object. The caller then register one or more callbacks on this Promise object. Those callbacks will be invoked when the asynchronous computation is done.

1
2
3
4
5
6
7
8
// before
function getRepsonse(params, callback)

// after
function getResponse(params) {
    return Promise(...)
}
getResponse.then(callback1, callback2)

With Promise, we can:

  1. avoid nested callbacks
  2. error handling - when we use callbacks before, there is no way to propagate the exception back to the intiator of the asynchronous operation. Promise provides a standarized way to handle erros and propagate errors correctly through a chain of promises.

Using promises

1
2
3
getJSON(url).then(jsonData => {
    ...
})

getJSON() starts an asynchronous HTTP request for the URL, while that request is pending, it returns a Promise object.

The Promise object defines a then() instance method. Instead of passing the callback function directly to getJSON(), we pass it to the then() method. When the HTTP response arrives, the body of that response parsed as JSON and is passed to the function that we passed to then().

Handling errors:

1
getJSON(URL).then(displayUserProfile).catch(handleError)

Chaining promises

A hypothetical Promise chain looks like below:

1
2
3
4
5
6
7
8
9
fetch(documentURL)
    .then(response => response.json())
    .then(document => {
        return render(document)
    })
    .then(rendered => {
        cacheInDatabase(rendered);
    })
    .catch(error => handle(error))

Earlier we saw the XMLHttpRequest object used to make a Http request. It’s a strangely named object and has awkward API. Now, we have Promise based Fetch API.

1
2
3
4
5
6
fetch("/api/user/profile").then(response => {
    if (response.ok && response.headers.get("Content-Type")) {
        // do something
    }
})

When the Promise returned by fetch() is fulfilled, it passes a Response object to the function you passed to its then() method.

However, the body of the response may not yet arrived. So the methods like text() and json() of the response return Promises. Here is the naive way of using fetch() and response.json() to get the body of an HTTP response.

1
2
3
4
5
6
7
fetch("/api/user/profile").then(response => {
    if (response.ok && response.headers.get("Content-Type")) {
        response.json().then(profile -> {
            displayUserProfile(profile);
        })
    }
})

This naive way nest them again. So a preferred idiom is to use Promises in a sequential chain with code like this:

1
2
3
4
5
6
7
fetch("/api/user/profile").then(response => {
    if (response.ok && response.headers.get("Content-Type")) {
        return response.json();
    }
}).then(profile => {
    displayUserProfile(profile);
})

We can see the first .then() returns a promise for the second .then() to fulfill or reject.

Resolving promises

We can write above chain as below:

1
2
3
fetch(theURL)
    .then(callback1)
    .then(callback2)

To make it more explicit:

1
2
3
4
5
6
7
8
9
10
11
12
function c1(response) {
    let p4 = response.json()
    return p4
}

function c2(profile) {
    displayUserProfile(profile);
}

p1 = fetch(theURL);
p2 = p1.then(c1);
p3 = p2.then(c2);