Javascript is single-threaded, only one statement is executed at a time. If something in our code is a long process it will freeze our application (block the thread) until it finishes. But as I explained in my previous articles, JavaScript does not work alone in the browser, we have the web API which is a set of features that help us run code asynchronously, a code that runs with callbacks in the background and does not freeze our application.
Side note: before reading this article I highly recommend that you will read 2 of my previous articles that will help you understand how JavaScript and Web API work under the hood: JavaScript Engine and JavaScript Runtime.
JavaScript has:
- memory heap -Place to store and write information. like allocate memory, use memory and release memory.
- Call stack – Place to keep track of where we are in the code to run the code in order.
JavaScript environment has:
- WEB API such ad DOM, AJAX, and Timeout.
- Callback Queue/Task Queue which holds the callback of asynchronous operations such as Ajax and setTimeout.
- Event Loop checks if the call stack is empty and when it is, the event loop moves the callback when they are ready, from the callback queue to the call stack.
// Let’s see an example that is using all the features I mentioned and the surprising result:
console.log(‘1’); // sync.
setTimeout(() => { // async - going to the Web API and to Callback Queue.
console.log(‘2’);
}, 0 );
console.log(‘3’); // sync.
// 1,3,2 will be printed
The setTimeout is a Web API feature when the JavaScript engine goes over this code it sent it to the JavaScript runtime where there it waits in the callback queue and from there through the event loop into the call stack when it’s empty. so by the time the callback of setTimeout runs, the 1 and 2 have been printed already.
Promises
A promise is an object that may produce a single value sometime in the future, either a resolved value or a reason that it’s not resolved (rejected). a promise may be in one of 3 states: fulfilled, rejected, or pending.
Before promises, we used callback which made the code less readable and more repetitive a code that is not easy to work with and maintain.
The example below shows what the above code will look like with promises. you can right away see how much readable it is:
doSomething1(1)
.then(() => doSomething2(2))
.then(() => doSomething3(3))
When ES6 came out, the native promises came out with it. native promises exist because of a new piece that was added to the JavaScript runtime, the micro Queue/Job Queue, and because of a change that was made to the event loop.
The micro task queue is smaller than the callback queue but has a higher priority. it means that the event loop when the call stack is empty, will check the micro task queue and after that, it will check the callback queue. let’s see an example which demonstrate this behavior:
setTimeout(() => {
console.log('async callback 1');
}, 0);
setTimeout(() => {
console.log('async callback 2');
}, 10);
Promise.resolve('async promise').then((data) => console.log(data));
console.log('sync code');
/*
Will be printed:
sync code
async promise
async callback 1
async callback 2
*/
Let’s see how can we create our own promises and by that understand them better:
// Creating a promise
const promise = new Promise((resolved, reject) => {
// some work…
If (true) {
resolve(‘Big Success’);
} else {
reject(‘Error’);
}
})
// Using the promise.
promise .then(result => console.log(result)); // Big Success.
// We can use chain more "then" to update the resolved value from the promise like this:
Promise
.then(result => result + ‘ Yoo Hoo’);
.then(result2 => {
console.log(result2); // Big Success Yoo Hoo.
})
// With “.catch” we can can catch any errors that may happen between the chains of ‘.then’ that above the ‘.catch’.
Promise
.then(result => result + ‘ Yoo Hoo’);
.then(result2 => {
Throw Error
console.log(result2); // Big Success Yoo Hoo.
})
.catch(() => console.log(‘Error’)) // Error
Let’s see a real-world example. we will make a fetch request from 3 sources and we will use the “Promise.all” to do something after all the promises have been resolved:
const urls = [
'https://jsonplaceholder.typicode.com/users',
'https://jsonplaceholder.typicode.com/posts',
'https://jsonplaceholder.typicode.com/albums'
];
Promise.all(urls.map(url => {
return fetch(url).then(resp => resp.json())
})).then(results => {
console.log(results[0]); // users array
console.log(results[1]); // posts array
console.log(results[2]); //albums array
}).catch(() => console.log('error')); // if one of the promises will fail, promise.all will move to the catch block means its rejected'
// 3 arrays will be printed.
“Promis.allSettled()” (which came out in ES2020) unlike “Promise.all”, does not care about “resolve” and “reject”. it will run all promises regardless if they reject or not, so the promise only comes back when all promises that have been added to it are complete. Let’s see an example:
const promise1 = new Promise((resolve, reject) => setTimeout(resolve, 5000));
const promise2 = new Promise((resolve, reject) => setTimeout(reject, 2000));
Promise
.allSettled([promise1,promise2])
.then(data => console.log(data))
.catch(e => console.log('Error', e));
// Result is an array with 2 objects for each promise.
Sometimes we need to do some operation after running multiple promises no matter if they are rejected or resolved. right after all promises are finished we will do some operation. for this we have the “.finally” method:
const urls = [
'https://jsonplaceholder.typicode.com/users',
'https://jsonplaceholder.typicode.com/posts',
'https://jsonplaceholder.typicode.com/albums'
];
Promise.all(urls.map(url => {
return fetch(url).then(resp => resp.json())
}))
.then(results => {
console.log(results[0]);
console.log(results[1]);
console.log(results[2]);
})
.catch((err) => console.log('error', err))
.finally((data) => console.log('extra', data))
Async Await (ES8)
Async-Await came out with ES8 and it’s built on top of promises. . an async function is a function that returns a promise and this syntax it’s easier to read because it looks like a synchronized code.
Let’s see the difference between the syntax of promises and async-await:
// Promises
doSomething1(1)
.then(() => doSomething2(2))
.then(() => doSomething3(3))
.then(() => doSomething3(4))
// Async-Await
async function playerStart() {
await doSomething2(1);
await doSomething2(2);
await doSomething2(3);
await doSomething2(4);
}
Both of the examples will run the code sequential, first doSomething1 and when resolve, doSomething2 will run, and so on.
In the aync-await example, we declare a function with the “async” keyword. by doing that we can use the await keyword in the function. the await keyword pause the function until there is a result. so the code below the await will not run until there is a result from the function with the await in front of it. in other words, we can use the await keyword in front of any function that returns a promise and once the promise is resolved then the function moves to the next line.
Let’s see another example:
// Promises
fetch('https://jsonplaceholder.typicode.com/users')
.then(resp => resp.json())
.then(console.log)
// Async-Await
async function fetchUsers() {
const resp = await fetch('https://jsonplaceholder.typicode.com/users');
const data await resp.json();
console.log(data);
}
Let’s see one last example:
const urls = [
'https://jsonplaceholder.typicode.com/users',
'https://jsonplaceholder.typicode.com/posts',
'https://jsonplaceholder.typicode.com/albums'
];
// Promises
Promise.all(urls.map(url => {
return fetch(url).then(resp => resp.json())
})).then(array => {
console.log(array [0]); // users array
console.log(array [1]); // posts array
console.log(array [2]); //albums array
}).catch('error'); // if one of the promises will fail, promise.all will
// Async-Await
const getData = async function() {
const [users, posts, albums] = await Promise.all(urls.map(url =>
fetch(url).then(resp => resp.json())
));
console.log(users); // users array
console.log(posts); // posts array
console.log(albums); //albums array
}
getData();
To catch errors with async-await we have to use the try-catch block:
const getData = async function() {
try {
const [users, posts, albums] = await Promise.all(urls.map(url =>
fetch(url).then(resp => resp.json())
));
console.log(users); // users array
console.log(posts); // posts array
console.log(albums); //albums array
} carch (err){
console.log('error', err);
}
}
getData();
Parallel, Sequential, and Race
When working with multiple promises we might need different behavior for order of executing them, let’s examine them:
- parallel – run promises all at the same time.
- sequential – run the first promise and when succeed run the second one and so on.
- race – run all promises and when one of them comes first, ignore the rest.
Let’s see these methods in action:
// Return a promise with a delay.
const promisify = (item, delay) =>
new Promise((resolve) =>
setTimeout(() =>
resolve(item), delay));
// Creating 3 functions that return a promise.
const a = () => promisify ('a', 100);
const b = () => promisify ('b', 1000);
const c = () => promisify ('c', 2000);
// With Promise.all we run all the promises at same time.
async function parallel() {
const promises = [a(), b(), c()];
const [output1, output2, output3] = await Promise.all(promises)
return `parallel is done: ${output1} ${output2} ${output3}`
}
//parallel().then(data => console.log(data));
parallel().then(console.log); // parallel is done: a,b,c
// With Promise.race we run all the promises at same time and return only the first one that comes.
async function race() {
const promises = []a(), b(), c();
const output1 = await Promise.race(promises);
return `race is done: ${output1}`;
}
race().then(console.log); // race is done: a
// The first will run and when finished the second will run
async function sequence() {
const output1 = await a();
const output2 = await b();
const output3 = await c();
return `sequence is done: ${output1} ${output2} ${output3}`;
}
sequence().then(console.log); // sequence is done: a,b, c
For Await Of
For -await-of allows us to loop through multiple async-await calls like we would loop with for-of on iterables. it loops over the async-await calls as if it was async code.
const urls = [
'https://jsonplaceholder.typicode.com/users',
'https://jsonplaceholder.typicode.com/posts',
'https://jsonplaceholder.typicode.com/albums'
];
const getData = async function() {
const arrayOfPromises = urls.map(url => fetch(url));
for await (let request of arrayOfPromises) {
const data = await request.json();
console.log(data);
}
}
getData();