🪴 yshalsager's Digital Garden

Search

Search IconIcon to open search

JavaScript Asynchronous

Last updated Aug 7, 2022

المعرفة:: JavaScript
الحالة:: #ملاحظة_مؤرشفة
المراجع:: The Complete JavaScript Course 2022 From Zero to Expert, https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch, https://javascript.info/promise-chaining, https://javascript.info/promise-api


# Synchronous vs Asynchronous

# Synchronous

1
2
3
4
const p = document.querySelector('.p');  
p.textContent = 'My name 1is Jonas!';  
alert('Text set!');  
p.style.color = 'red;  

# Asynchronous

1
2
3
4
5
const p = document.querySelector('.p');  
setTimeout(function () { // Asynchronous  
  p.textContent = 'My name is Jonas!';  
  }, 5000);  
p.style.color = 'red;  

# AJAX

# API

# XMLHttpRequest

1
2
3
4
5
6
7
8
const request = new XMLHttpRequest();  
request.open('GET', `https://restcountries.com/v3.1/name/egypt`);  
request.send();  
// Since XMLHttpRequest is async, we can't get request.responseText immediately, so we have to set an event listener to handle response once it's here.  
request.addEventListener('load', function () {  
  const [data] = JSON.parse(this.responseText);  
  console.log(data);  
});  
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
setTimeout(() => {  
  console.log('1 second passed');  
  setTimeout(() => {  
    console.log('2 seconds passed');  
    setTimeout(() => {  
      console.log('3 second passed');  
      setTimeout(() => {  
        console.log('4 second passed');  
      }, 1000);  
    }, 1000);  
  }, 1000);  
}, 1000);  

^callbackHellExample

# Promises

# The Promise Lifecycle

  1. In the very beginning, we say that a promise is pending. This is before any value resulting from the asynchronous task is available. During this time, the asynchronous task is still doing its work in the background.
  2. When the task finally finishes, we say that the promise is settled. There are two different types of settled promises and that’s fulfilled promises and rejected promises.
    • A fulfilled promise is a promise that has successfully resulted in a value just as we expect it. To handle this fulfilled state, we can use the then() method and pass a callback function that we want to be executed as soon as the promise is actually fulfilled.
    • A rejected promise means that there has been an error during the asynchronous task.
    • A promise is only settled once, then the state will remain unchanged forever.
    • finally() method can be used to do something that always needs to happen no matter the result of the promise, for example, hiding a loading spinner.
graph TB Pending[Pending
Before the future value is available] Settled[Settled
Asynchronoushas task has finished] Fullfilled[Fullfilled
Success The value is now available] Rejected[Rejected
An error happened] Pending -- Async Task --> Settled Settled --> Fullfilled & Rejected

# Fetch API

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// const request = new XMLHttpRequest();  
// request.open('GET', `https://restcountries.com/v3.1/name/egypt`);  
// request.send();  
  
// Fetch returns promises  
const req = fetch('https://restcountries.com/v3.1/name/egypt')  
console.log(req) // A promise  
  
// Fullfilled promises can be handled using .then()  
fetch('https://restcountries.com/v3.1/name/egypt').then((response) => console.log(response)) // A Response object  
  
// Get JSON  
fetch('https://restcountries.com/v3.1/name/egypt').then((response) => response.json())  
  .then((data) => console.log(data));  

# Chaining Promises

graph TB 1[fetch] 2[.then] 3[.then] 1-->2-->3
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const getCountryData = function (country) {  
  // Country 1  
  fetch(`https://restcountries.com/v3.1/name/${country}`)  
    .then(response => response.json())  
    .then(data => {  
      renderCountry(data[0]);  
      const neighbour = data[0].borders[0];  
      if (!neighbour) return;  
      // Country 2  
      return fetch(`https://restcountries.com/v3.1/alpha/${neighbour}`);  
    })  
    .then(response => response.json())  
    .then(data => renderCountry(data, "neighbour"));  
  getCountryData("portugal");  
};  

# Handling Promise Rejection

There are two ways of handling rejections:

1
2
3
fetch(`https://restcountries.com/v3.1/name/${country}`).then(  
  response => response.json(), err => alert(err)  
);  
1
fetch(...).then(response => response.json()).then(data => console.log(data)).catch(err => alert(err));  

# Throwing Errors Manually

1
2
3
4
5
6
fetch(`https://restcountries.com/v3.1/alpha/asdfa`).then(response => {  
  if (!response.ok)  
    throw new Error(`Country not found (${response.status})`);  
  
  return response.json();  
});  
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
const getJSON = function (url, errorMsg = 'Something went wrong') {  
  return fetch(url).then(response => {  
    if (!response.ok) throw new Error(`${errorMsg} (${response.status})`);  
  
    return response.json();  
  });  
};  
  
const getCountryData = function (country) {  
  // Country 1  
  getJSON(  
    `https://restcountries.com/v3.1/name/${country}`,  
    'Country not found'  
  )  
    .then(data => {  
      renderCountry(data[0]);  
      const neighbour = data[0].borders[0];  
  
      if (!neighbour) throw new Error('No neighbour found!');  
  
      // Country 2  
      return getJSON(  
        `https://restcountries.com/v3.1/alpha/${neighbour}`,  
        'Country not found'  
      );  
    })  
  
    .then(data => {  
      [data] = data;  
      renderCountry(data, 'neighbour')  
    })  
    .catch(err => {  
      console.error(`${err} 💥💥💥`);  
      renderError(`Something went wrong 💥💥 ${err.message}. Try again!`);  
    })  
    .finally(() => {  
      countriesContainer.style.opacity = 1;  
    });  
};  

# How Asynchronous works?

# Introduction

Previously, we learnt that:

Remember that one of JavaScript features is the non-blocking event loop:

, JavaScript in depth > 9 Non-blocking event loop

# How Asynchronous JavaScript Works Behind the Scenes?

How can asynchronous code be executed in a non-blocking way, if there is only one thread of execution in the engine?

# The Event Loop

The event loop concept is very simple. There’s an endless loop, where the JavaScript engine waits for tasks, executes them and then sleeps, waiting for more tasks.

# Event Loop and Promises

# Event Loop in Practice

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
console.log('Test start');  
setTimeout(() => console.log('0 sec timer'), 0);  
Promise.resolve('Resolved promise 1').then(res => console.log(res));  
Promise.resolve('Resolved promise 2').then(res => {  
  for (let i = 0; i < 1000000000; i++) {}  
  console.log(res);  
});  
console.log('Test end');  
// output  
// 1. Code outside of a callback will run first, from top to botton.  
// Test start  
// Test end  
// 2. Both timer and promise will finish at the exact same time,  
// Resolved promise 1 // but since Promise is put in micro tasks queue, it will be handled first.  
// Resolved promise 2 // Promise 2 is shown after than, even though it takes a lot of time due to heavy computing task.  
// 0 sec timer // finally, code from regular callback queue is executed.  

# Building Promises

Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// Build a Simple Promise  
const guessPromise = new Promise(function (resolve, reject) {  
  console.log('Guess draw is happening');  
  setTimeout(function () {  
    if (Math.floor(Math.random() * 10) + 1 >= 5) {  
      resolve('You Won!');  
    } else {  
      // reject('You lost!');  
      reject(new Error('You lost!'));  
    }  
  }, 2000);  
});  
  
// Consume the Promise  
guessPromise.then(res => console.log(res)).catch(err => console.error(err));  

# Promisifying

# Example 1

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// Promisifying setTimeout  
const wait = function (seconds) {  
  // We don't need the reject function because it's actually impossible for the timer to fail.  
  return new Promise(function (resolve) {  
  // We also are not going to pass any resolved value into the resolve function because that's actually not mandatory.  
    setTimeout(resolve, seconds * 1000); // setTimeout needs time in milliseconds  
  });  
};  
  
const waitA = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1000));  
  
waitA(2).then(() => console.log('2 second passed from calling waitA'));  
  
wait(1)  
  .then(() => {  
    console.log('1 second passed');  
    return wait(1); // return a new promise to chain calling  
  })  
  .then(() => {  
    console.log('2 second passed');  
    return wait(1);  
  })  
  .then(() => {  
    console.log('3 second passed');  
    return wait(1);  
  })  
  .then(() => console.log('4 second passed'));  
  
// 1 second passed  
// 2 second passed from calling waitA  
// 2 second passed  
// 3 second passed  
// 4 second passed  

^callbackHellExample

# Example 2

Remember that navigator.geolocation.getCurrentPosition() , JavaScript Geolocation API &gt; ^getCurrentPositionParameters So we don’t need to pass resolve and reject manually.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// Promisifying the Geolocation API  
const getPosition = function () {  
  return new Promise(function (resolve, reject) {  
    // navigator.geolocation.getCurrentPosition(  
    //   position => resolve(position),  
    //   err => reject(err)  
    // );  
    navigator.geolocation.getCurrentPosition(resolve, reject);  
  });  
};  
getPosition().then(pos => console.log(pos));  

# Example 3

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const imgContainer = document.querySelector('.images');  
const wait = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1000));  
  
// Promisifying img loading  
const createImage = function (imgPath) {  
  return new Promise(function (resolve, reject) {  
    const img = document.createElement('img');  
    img.src = imgPath;  
  
    img.addEventListener('load', function () {  
      imgContainer.append(img);  
      resolve(img);  
    });  
  
    img.addEventListener('error', function () {  
      reject(new Error('Image not found'));  
    });  
  });  
};  
  
// Show image for two seconds, then hide it and show a second image for two seconds as well, then hide it.  
  
let currentImg;  
  
createImage('img/img-1.jpg')  
  .then(img => {  
    currentImg = img;  
    console.log('Image 1 loaded');  
    return wait(2);  
  })  
  .then(() => {  
    currentImg.style.display = 'none';  
    return createImage('img/img-2.jpg');  
  })  
  .then(img => {  
    currentImg = img;  
    console.log('Image 2 loaded');  
    return wait(2);  
  })  
  .then(() => {  
    currentImg.style.display = 'none';  
  })  
  .catch(err => console.error(err));  

# Creating an immediately fulfilled or a rejected promise

1
2
3
Promise.resolve('abc').then(x => console.log(x)); // abc  
Promise.reject('abc').catch(x => console.error(x)); // abc  
Promise.reject(new Error('Problem!')).catch(x => console.error(x)); // Error: Problem!  

# Consuming Promises with async and await

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
async function whereAmI(country) {  
  // fetch(`https://restcountries.com/v3.1/name/${country}`).then(res => console.log(res)) // old way  
  const res = await fetch(`https://restcountries.com/v3.1/name/${country}`);  
  console.log(res);  
}  
whereAmI("Egypt");  
console.log("1");  
  
// output  
// 1  
// [object Response]  

Full example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const getPosition = function () {  
  return new Promise(function (resolve, reject) {  
    navigator.geolocation.getCurrentPosition(resolve, reject);  
  });  
};  
  
const whereAmI = async function() {  
  // Geolocation  
  const pos = await getPosition();  
  const {  
    latitude: lat,  
    longitude: lng  
  } = pos.coords;  
  
  // Reverse geocoding  
  const resGeo = await fetch(`https://geocode.xyz/${lat},${lng}?geoit=json`);  
  const dataGeo = await resGeo.json();  
  console.log(dataGeo);  
  
  // Country data  
  const res = await fetch(  
    `https://restcountries.com/v3.1/alpha/${dataGeo.prov}`  
  );  
  const data = await res.json();  
  console.log(data);  
  renderCountry(data[0]);  
};  
  
whereAmI();  

# Error Handling with async and await

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const whereAmI = async function () {  
  try {  
    // Geolocation  
    const pos = await getPosition();  
    const { latitude: lat, longitude: lng } = pos.coords;  
  
    // Reverse geocoding  
    const resGeo = await fetch(`https://geocode.xyz/${lat},${lng}?geoit=json`);  
    if (!resGeo.ok) throw new Error('Problem getting location data');  
    const dataGeo = await resGeo.json();  
    console.log(dataGeo);  
  
    // Country data  
    const res = await fetch(  
      `https://restcountries.com/v3.1/alpha/${dataGeo.prov}`  
    );  
    if (!res.ok) throw new Error('Problem getting country');  
    const data = await res.json();  
    console.log(data);  
    renderCountry(data[0]);  
  } catch (err) {  
    console.error(`${err} 💥`);  
    renderError(`💥 ${err.message}`);  
  }  
};  

# Returning Values from async Functions

1
2
3
4
whereAmI()  
  .then(city => console.log(`2: ${city}`))  
  .catch(err => console.error(`2: ${err.message} 💥`))  
  .finally(() => console.log('3: Finished getting location'));  
1
2
3
4
5
6
7
8
9
async function () {  
  try {  
    const city = await whereAmI();  
    console.log(`2: ${city}`);  
  } catch (err) {  
    console.error(`2: ${err.message} 💥`);  
  }  
  console.log('3: Finished getting location');  
})();  

# Promise Combinator Functions

# Running Promises in Parallel with Promise.all()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const get3Countries = async function (c1, c2, c3) {  
  try {  
    // The commented code runs in sequence not in parallel  
    // const [data1] = await getJSON(  
    //   `https://restcountries.com/v3.1/name/${c1}`  
    // );  
    // const [data2] = await getJSON(  
    //   `https://restcountries.com/v3.1/name/${c2}`  
    // );  
    // const [data3] = await getJSON(  
    //   `https://restcountries.com/v3.1/name/${c3}`  
    // );  
    // console.log([data1.capital, data2.capital, data3.capital]);  
  
    const data = await Promise.all([  
      getJSON(`https://restcountries.com/v3.1/name/${c1}`),  
      getJSON(`https://restcountries.com/v3.1/name/${c2}`),  
      getJSON(`https://restcountries.com/v3.1/name/${c3}`),  
    ]);  
    console.log(data.map(d => d[0].capital)); // ["City1", "City2", "City3"]  
  } catch (err) {  
    console.error(err);  
  }  
};  
get3Countries('portugal', 'canada', 'tanzania');  

# Promise.race()

1
2
3
4
5
6
7
8
9
// Promise.race  
(async function () {  
  const res = await Promise.race([  
    getJSON(`https://restcountries.com/v3.1/name/italy`),  
    getJSON(`https://restcountries.com/v3.1/name/egypt`),  
    getJSON(`https://restcountries.com/v3.1/name/mexico`),  
  ]);  
  console.log(res[0]);  
})();  
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
  
const timeout = function (sec) {  
  return new Promise(function (_, reject) {  
    setTimeout(function () {  
      reject(new Error('Request took too long!'));  
    }, sec * 1000);  
  });  
};  
  
Promise.race([  
  getJSON(`https://restcountries.com/v3.1/name/tanzania`),  
  timeout(5),  
])  
  .then(res => console.log(res[0]))  
  .catch(err => console.error(err));  

# Promise.allSettled()

1
2
3
4
5
6
// Promise.allSettled  
Promise.allSettled([  
  Promise.resolve('Success'),  
  Promise.reject('ERROR'),  
  Promise.resolve('Another success'),  
]).then(res => console.log(res));  

# Promise.any()

1
2
3
4
5
6
7
8
// Promise.any [ES2021]  
Promise.any([  
  Promise.resolve('Success'),  
  Promise.reject('ERROR'),  
  Promise.resolve('Another success'),  
])  
  .then(res => console.log(res))  
  .catch(err => console.error(err));  

# Top-Level await

1
2
3
4
5
// Top-Level Await (ES2022)  
console.log('Start fetching');  
const res = await fetch('https://jsonplaceholder.typicode.com/posts');  
const data = await res.json();  
console.log(data);