- COMP.CS.200
- 7. JavaScript: FP & async (assignment)
- 7.5 Asynchronous JavaScript
Asynchronous JavaScript¶
JavaScript is single-threaded though asynchronous and non-blocking. How is that achieved?
When a task is being processed in the main thread nothing else can be done at the same time. Let’s take a look at some blocking code:
const btn = document.querySelector('button');
btn.addEventListener('click', () => {
// Function execution stops here until user
// interacts with the alert
alert('You clicked me!');
// Code proceeds here once user has interacted with the alert
let pElem = document.createElement('p');
pElem.textContent = 'This is a newly-added paragraph.';
document.body.appendChild(pElem);
});
What will happen, when a button is clicked ?
An alert is created. Nothing can be done until a user clicks the alert.
A new paragraph is added to the page
https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Introducing
When you run a task that will take some time to complete, you should use worker threads instead of main thread. This way a user can still interact with a page, while the task is being processed.
Callbacks¶
What exactly are these so called callbacks?¶
Callback is a way to delay function execution. Let’s take a look at following code example:
function add(x, y) {
return x + y;
};
function addFive(x, referenceToAdd) {
return referenceToAdd(5, x);
};
addFive(5, add); // 10
Function add
takes in two arguments x
and y
and then returns their sum.
Function addFive
takes in two arguments x
and a reference to another function.
It then calls the function that was passed in with values x
and 5.
Function addFive
is higher-order function because it takes a function
reference as a parameter.
Function add
is a callback when it is passed to addFive
as an argument.
function add(x, y) {
return x + y;
}
function higherOrderFunction(x, callback) {
return callback(5, x);
}
higherOrderFunction(5, add);
What are the cons of using callbacks?¶
In real-life scenarios you will often make lots of API calls to fetch data and images from databases. Sometimes you will have to for example:
Fetch user data
Fetch something else based on the user data
Fetch something else based on the results of step 2.
This doesn’t sound too complicated but it will start to spiral out of control rather quickly. Let’s take a look at an example with just steps 1 and 2. First we will retrieve the user data from API and once we have it we will retrieve user blog posts. (We use a free testing API which does not contain real user data but some fake data for testing purposes.)
const baseUrl = "https://jsonplaceholder.typicode.com";
const postsUrl = `${baseUrl}/posts`;
const usersUrl = `${baseUrl}/users`;
const userId = 1;
document.getElementById("fetchBtn").addEventListener("click", () => {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
if (xhr.status == 200) {
document.getElementById("userData").innerText = xhr.responseText;
const xhr2 = new XMLHttpRequest();
xhr2.onreadystatechange = function() {
if (xhr2.readyState == 4) {
if (xhr2.status == 200) {
document.getElementById("postsData").innerText = xhr2.responseText;
} else {
console.error("Unsuccessful transfer (error code: " + xhr.statusText + ")");
}
}
}
xhr2.open("GET", `${postsUrl}?userId=${userId}`, true)
xhr2.send();
} else {
console.error("Unsuccessful transfer (error code: " + xhr2.statusText + ")");
}
}
}
xhr.open("GET", `${usersUrl}/${userId}`, true)
xhr.send();
})
Now imagine if you had to add a third or fourth retrieval. You’ll quickly end up with the Pyramid of DOOM or what is sometimes also referred to as Callback Hell.
Promises¶
Promises were developed to make it easier to write asynchronous code. Promise object represents the eventual value or failure of asynchronous operation.
Promise is a value that is not necessarily known when it is created. Promise has three different states:
Pending
Promise has not yet been resolved
Fulfilled
Promise was successful
The value is usable
Rejected
The operation failed
Promise constructor takes in one argument, a callback function with two parameters, resolve and reject which are both callbacks themselves.
const myFirstPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Success!");
}, 1000);
});
myFirstPromise.then((successMessage) => {
console.log("Yay! " + successMessage);
});
We can do async operations inside the Promise. When we have done everything we need to do we can call resolve. If something goes wrong we will need to call reject.
Promise.prototype.then
and Promise.prototype.catch
return promises
so they can be chained.
In our example above we create a new Promise that will resolve after waiting
for 1000ms. When executed we first receive a promise in pending state.
After one second the promise resolves and we move on the the console.log
.
Next let’s take a look at the previous example of retrieving data from an API.
const baseUrl = "https://jsonplaceholder.typicode.com";
const postsUrl = `${baseUrl}/posts`;
const usersUrl = `${baseUrl}/users`;
const userId = 1;
document.getElementById("fetchBtn").addEventListener("click", function() {
fetch(`${usersUrl}/${userId}`)
.then((response) => {
return response.json();
})
.then((user) => {
document.getElementById("userData").innerText = JSON.stringify(user);
return user;
})
.then((user) => {
fetch(`${postsUrl}?userId=${user.id}`)
.then((response) => {
return response.json();
})
.then((posts) => {
document.getElementById("postsData").innerText = JSON.stringify(posts);
})
})
.catch((error) => {
console.error(error.message);
})
})
With promises it’s slightly more human-readable. Quite many libraries provide a possibility to use promises for their async operations. JavaScript Fetch is available from almost all environments making it a good choice for retrieving data from APIs.
So promises are more readable than callbacks but they’re still not perfect. Many non-JavaScript developers struggle with the concept of promises. That is why there is more syntactic sugar developed to hide the complexity of promises.
Exercise¶
Attention
This exercise should be done in groups and submitted as a group.
Include all group members and make sure that everyone knows how your solution works. Only one group member needs to submit the exercise on behalf of the whole group.
You are going to build a function myFirstPromise
that will take shouldFail
as a parameter. This function will return a Promise
.
If
shouldFail
is true the Promise willreject
with return value ofFail
.If
shouldFail
is false the Promise willresolve
with return value ofSuccess
.
A+ presents the exercise submission form here.
Async/Await¶
One can declare a function to be asynchronous by using the async
keyword:
async function example() {}
const example = async () => {};
Async functions operate in a different order from regular functions via the event-loop. The return value of an async function is an implicit promise.
Inside async function we can use await
keyword.
Await pauses the function execution and waits for the promise to be resolved.
Let’s run previous example that resolves after one second with async keyword:
function resolveAfter1Second() {
return new Promise(resolve => {
setTimeout(function() {
resolve("success!")
}, 1000)
})
}
const runAsync = async () => {
const response = await resolveAfter1Second();
console.log("The operation was " + response);
};
runAsync(); // The operation was success!
We can declare the promises exactly like before and then wait for their execution in async functions.
Next let’s take a look at our API example. Can it be improved by using async/await?
const baseUrl = "https://jsonplaceholder.typicode.com";
const postsUrl = `${baseUrl}/posts`;
const usersUrl = `${baseUrl}/users`;
const userId = 1;
const fetchAsync = async url => {
const response = await fetch(url);
const data = await response.json();
return data;
}
document.getElementById("fetchBtn").addEventListener("click", async () => {
const userData = await fetchAsync(`${usersUrl}/${userId}`);
document.getElementById("userData").innerText = JSON.stringify(userData);
const userPosts = await fetchAsync(`${postsUrl}?userId=${userData.id}`);
document.getElementById("postsData").innerText = JSON.stringify(userPosts);
});
Wow, that’s looking way cleaner!
First let’s take a look at the fetchAsync
function. Remember from our previous
examples that the fetch returned a promise and that we can await for
a promise to resolve? Now we can fetch data from any URL in three lines with
a reusable function.
Because we define the callback function of fetchBtn
to be asynchronous
we can use await in there.
Async/await makes the code easier to read, faster to write and more intuitive to test.
Promise.all¶
Our previous examples have been about sequentially retrieving data:
Fetch user 1
Fetch posts for user 1
What if we had to fetch data for users 1, 2 and 3? What if there were a hundred users?
Promise.all is great for retrieving data in parallel. It takes an iterable as an argument and returns a Promise. This Promise is fulfilled when everything in the iterable has been fulfilled.
In other words you typically pass an array of promises for Promise.all. Your Promise.all fulfills when every promise in the array of promises has been fulfilled.
Promise.all rejects if any of the promises in the array reject.
Let’s modify our example a little bit. Instead of fetching a user and then information about the posts of that user, we could fetch two users and display their data. These requests can be done in parallel with Promise.all.
const baseUrl = "https://jsonplaceholder.typicode.com";
const usersUrl = `${baseUrl}/users`;
const usersArray = [1, 2];
const fetchAsync = async url => {
const response = await fetch(url);
const data = await response.json();
return data;
}
document.getElementById("fetchBtn").addEventListener("click", async () => {
// Notice how the map operation takes in an async function which always
// returns promises and usersData will therefore be an array of promises
const usersData = usersArray.map(async user => fetchAsync(`${usersUrl}/${user}`));
Promise.all(usersData).then(completed => {
completed.map((user, index) => {
document.getElementById(`userData${index + 1}`).innerText = JSON.stringify(user);
});
});
})
usersData is an array of promises. When we pass usersData as an argument to Promise.all we receive a single promise. When that single promise resolves we move on to the then statement where we have the data available.
Closing words and more resources¶
This chapter introduced many quite complex concepts in a short amount of time.
The asynchronous nature of JavaScript confuses many new web developers and causes tons of unexpected behaviours which in turn causes the same questions to appear in StackOverflow over and over again. You should practice using asynchronous JavaScript and read more about it.
Here is a great slow-paced YouTube tutorial about asynchronous JavaScript.