Asynchronous JavaScript
Overview
- callbacks: example setTimeout
- asynchronous IO: ajax
- from callbacks to Promises
- async await
pdf
Problem statement
Problem statement:
- do something
- ask for processing that will take too long to wait and return a value
- do something with that value
Extended problem:
- do something
- ask for processing that will take too long to wait and return a value
- do something else that is independant
- do something with that value
Extended extended problems:
- do this with multiple async actions
- wait for multiple results
Callbacks
- JavaScript has features that help:
- the function to help with capturing code to be executed later: a function is a set of coherent instructions that can be triggered, e.g. at the end of a wait
- the function can be defined inside another function to have a specific variable context (closure)
- Example: code to load a file and find the last occurrence of a string in it then call function fun on this information
function f(fileName, searchString, fun) {
fs.readFile(fileName, function(err, data) {
if (err) {
// do something to process the error
}
if (data) {
fun(data.lastIndexOf(searchString));
}
});
}
Warnings
- returning a value is not possible, you can only provide a function to consume the value when ready.
- order of execution is not obvious
function f(fileName, searchString, fun) {
fs.readFile(fileName, function(err, data) {
if (err) {
// do something to process the error
}
if (data) {
fun(data.lastIndexOf(searchString));
}
});
}
Another example: animations
function animate() {
// do something
...
setTimeout(animate, 100); // call myself in 100ms
}
This function is called every 100ms after the first time, which you have to call to start.
Another example: Ajax
var xhr = new XMLHttpRequest();
xhr.open("GET", "test.txt");
xhr.onload = function() {
alert(this.responseText);
}
xhr.send();
- create the xhr object
- position the method and url
- define the callback
- start executing the background code
Consequences
- if result comes later, errors also come later, in a context they may not be understood: context needs to be kept not only for processing results
- debugging is a mess as the order of execution depends on things out of my control
- errors are not always predictable
- if you did not provide error recovery, errors can be really hard to understand
- you have to create your code in a way resistant to out-of-order results
- you may not have thought of all the actual dependencies
Promises
- Promises are a way to structure asynchronous code that is convenient/readable
- Promises are still new and you may find Promise code that is “legacy” (behaves differently)
- Using promises:
// f is a function that returns a promise when all its stuff is done
f()
.then(resultProcessing)
.catch(errorProcessing)
.finally(doItAnyWay)
function resultProcessing(result) {...}
function errorProcessing(error) {...}
function doItAnyWay() {...}
Why does this notation work ?
f().then(resultProcessing).catch(errorProcessing).finally(doItAnyWay)
- the function f is called and returns a pending promise
- then is called and returns a pending promise (same object as above)
- catch is called and returns a pending promise (same object as above)
- finally is called and returns a pending promise (same object as above)
- the code after that is executed until there is a thread break … After that, either:
- resultProcessing is called with the value passed to resolve, followed by doItAnyWay()
- or errorProcessing is called with the value passed to reject, followed by doItAnyWay()
Creating promises
- A Promise needs one function with two parameters:
- a function
resolve
called when the processing is successful, to pass the result of the processing on
- a function
reject
called when there is an error
- A Promise is in one of three states:
- pending (in progress)
- fulfilled (resolve has been called)
- rejected (reject has been called)
- To create a Promise:
let p = new Promise(function (resolve, reject) {...}) ;
// the anonymous function should call resolve with the result of
// the processing OR call reject with the reason (error)
Rewrite Ajax as Promise
function get(url) {
return new Promise((resolve, reject) => {
var xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.onload = () => resolve(xhr);
xhr.onerror = () => reject(xhr);
xhr.send();
});
}
Combining promises
Promise.all(iterable).then(…)
I have used this function on an array of promises (the iterable
) to wait for the completion of all the promises in the array
Promise.any(iterable).then(…)
This is the opposite of Promise.all, and then is executed with the value of the first promise that is resolved in the iterable
Async / await
- Async/await is actually just syntax sugar built on top of promises. It cannot be used with plain callbacks or node callbacks.
- Async/await is, like promises, non-blocking.
- Async/await makes asynchronous code look and behave a little more like synchronous code. This is where all its power lies.
Promise code:
const makeRequest = () =>
getJSON()
.then(data => {
console.log(data)
return "done"
})
makeRequest()
Async/await:
const makeRequest = async () => {
console.log(await getJSON())
return "done"
}
makeRequest()
Discussion of async/await vs Promise
- async/await is only relevant for code USING Promises, it seems you need to learn the Promises anyway if you need to write code that creates Promises…
- It is only syntax, but the syntax seems to be simpler in more complex cases, including the cases where multiple Promises are involved
- If you do not know Promises syntax, you may be better off learning the async syntax directly
- More complex cases seem to be A LOT simpler with async than with Promises
- It looks like on of those
des gouts et des couleurs...
cases
- One thing is clear: learn async/await and Promises
- async cannot be used at the top level, so the top level code has to be in Promise form