Skip to content

Mastering Asynchronous JavaScript with Async/Await πŸ—‘οΈ ​

Join Tanjiro and the Demon Slayer Corps as we delve into the powerful realm of Asynchronous JavaScript. We'll explore async functions, await expressions, top-level await, and the intricacies of handling promises with Promise.all and Promise.allSettled. Just like the relentless battles against demons, mastering asynchronous JavaScript requires precision, timing, and a deep understanding of the flow!

Understanding Asynchronous JavaScript ​

In the world of JavaScript, not everything happens instantly. Asynchronous operations allow your code to perform tasks like fetching data from an API without blocking the main thread. This ensures your application remains responsive and efficient.

Imagine asynchronous operations as Breathing Techniques πŸŒ€ used by the Demon Slayersβ€”allowing them to execute multiple maneuvers swiftly without delay.

The Power of async and await ​

The Basics of async Functions ​

Tanjiro's First async Technique

An async function is a function that returns a Promise. It allows you to write asynchronous code that looks synchronous, making it easier to read and maintain.

javascript
async function getCountryName1() {
  const res = await fetch("https://restcountries.com/v3.1/all");
  const countries = await res.json();
  const result = countries.map((country) => country.name.common);
  return result;
}

How It Works:

  1. async Keyword: Declares an asynchronous function.
  2. await Keyword: Pauses the function execution until the Promise is resolved.
  3. Return Value: The function returns a Promise that resolves to the returned value.

Top-Level await πŸš€ (Introduced in 2019) ​

Top-level await allows you to use await outside of async functions, simplifying the code further.

javascript
async function getCountryName() {
  const countries = await fetch("https://restcountries.com/v3.1/all").then(
    (res) => res.json()
  );
  const result = countries.map((country) => country.name.common);
  return result;
}

let result = await getCountryName();
console.log(result);

Key Differences:

  • Chaining with .then(): Combines await with .then() for handling Promises.
  • Top-Level Usage: Enables await at the top level, making the code cleaner.

Handling Multiple Promises with Promise.all and Promise.allSettled ​

Understanding Promise.all ​

Nezuko's Coordinated Defense

Promise.all takes an array of Promises and returns a single Promise that resolves when all of the input Promises resolve. If any Promise rejects, Promise.all rejects immediately.

javascript
async function getAllSettledPromise() {
  console.log("Case 2: P4 reject");
  let P4 = new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(2);
    }, 2000);
  });

  let P5 = new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(8);
    }, 1000);
  });

  let P6 = new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(3);
    }, 4000);
  });

  // Output & When?
  try {
    const result = await Promise.all([P4, P5, P6]);
    console.log("All settled: ", result);
  } catch (msg) {
    console.log("Oops: ", msg);
  }
}

getAllSettledPromise();

What Happens Here:

  • P4: Rejects after 2 seconds.
  • P5: Resolves after 1 second.
  • P6: Resolves after 4 seconds.
  • Outcome: Since P4 rejects, Promise.all immediately rejects, and the catch block is executed.

Using Promise.allSettled ​

Zenitsu's Comprehensive Strategy

Unlike Promise.all, Promise.allSettled waits for all Promises to settle (either fulfilled or rejected) and provides their outcomes.

javascript
async function getAllSettledPromise() {
  console.log("Case 2: P4 reject");
  let P4 = new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(2);
    }, 2000);
  });

  let P5 = new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(8);
    }, 1000);
  });

  let P6 = new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(3);
    }, 4000);
  });

  // Output & When?
  const results = await Promise.allSettled([P4, P5, P6]);
  console.log("All settled: ", results);
}

getAllSettledPromise();

Outcome:

  • P4: Rejected with reason 2.
  • P5: Fulfilled with value 8.
  • P6: Fulfilled with value 3.
  • Output:
    All settled: [
      { status: 'rejected', reason: 2 },
      { status: 'fulfilled', value: 8 },
      { status: 'fulfilled', value: 3 }
    ]

Additional Examples and Extended Topics ​

Example 1: Sequential vs. Parallel Execution ​

Inosuke's Dual Breathing Forms

Consider two asynchronous operations: fetching user data and fetching posts. Executing them sequentially vs. in parallel can impact performance.

Sequential Execution:

javascript
async function fetchUserAndPostsSequential() {
  const user = await fetchUser();
  const posts = await fetchPosts(user.id);
  return { user, posts };
}

Parallel Execution:

javascript
async function fetchUserAndPostsParallel() {
  const [user, posts] = await Promise.all([fetchUser(), fetchPosts()]);
  return { user, posts };
}

Comparison:

  • Sequential: Total time is the sum of both operations.
  • Parallel: Total time is the maximum time of the two operations.

Example 2: Error Handling with try...catch ​

Kanao's Protective Shields

Handling errors gracefully ensures your application remains robust.

javascript
async function fetchData() {
  try {
    const response = await fetch("https://api.example.com/data");
    if (!response.ok) {
      throw new Error("Network response was not ok");
    }
    const data = await response.json();
    return data;
  } catch (error) {
    console.error("Fetch error:", error);
  }
}

fetchData();

Extended Topic: async Iterators and Generators ​

Muzan's Complex Schemes

async iterators allow you to work with streams of asynchronous data.

javascript
async function* asyncGenerator() {
  const data = [1, 2, 3];
  for (const item of data) {
    await new Promise((resolve) => setTimeout(resolve, 1000));
    yield item;
  }
}

(async () => {
  for await (const num of asyncGenerator()) {
    console.log(num);
  }
})();

Output:

1
2
3

Each number is logged every second, demonstrating asynchronous iteration.

Tasks and Practice ​

Task 1: Implementing Promise.allSettled ​

Rengoku's Challenge

Modify the getAllSettledPromise function to use Promise.allSettled instead of Promise.all and handle the results accordingly.

Hint:

Use Promise.allSettled to retrieve the status of each Promise.

javascript
// Your Code Here
Answer
javascript
async function getAllSettledPromise() {
  console.log("Case 2: P4 reject");
  let P4 = new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(2);
    }, 2000);
  });

  let P5 = new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(8);
    }, 1000);
  });

  let P6 = new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(3);
    }, 4000);
  });

  // Using Promise.allSettled
  const results = await Promise.allSettled([P4, P5, P6]);
  console.log("All settled: ", results);
}

getAllSettledPromise();

Output:

Case 2: P4 reject
All settled: [
  { status: 'rejected', reason: 2 },
  { status: 'fulfilled', value: 8 },
  { status: 'fulfilled', value: 3 }
]

Task 2: Fetching Data with Sequential and Parallel Execution ​

Tengen's Dual Strategy

Create two functions, one that fetches user data and posts sequentially, and another that does so in parallel using Promise.all.

Hint:

Use the provided fetchUser and fetchPosts functions.

javascript
async function fetchUser() {
  const res = await fetch("https://jsonplaceholder.typicode.com/users/1");
  return res.json();
}

async function fetchPosts(userId) {
  const res = await fetch(
    `https://jsonplaceholder.typicode.com/posts?userId=${userId}`
  );
  return res.json();
}

// Implement fetchUserAndPostsSequential and fetchUserAndPostsParallel
Answer

Sequential Execution:

javascript
async function fetchUserAndPostsSequential() {
  const user = await fetchUser();
  const posts = await fetchPosts(user.id);
  return { user, posts };
}

fetchUserAndPostsSequential().then((data) => console.log(data));

Parallel Execution:

javascript
async function fetchUserAndPostsParallel() {
  const [user, posts] = await Promise.all([fetchUser(), fetchPosts()]);
  return { user, posts };
}

fetchUserAndPostsParallel().then((data) => console.log(data));

Task 3: Error Handling in Async Functions ​

Gyomei's Resilience Training

Enhance the fetchData function to retry fetching data up to three times if it fails.

Hint:

Use a loop to attempt fetching and catch errors.

javascript
async function fetchDataWithRetry(url, retries = 3) {
  // Your Code Here
}
Answer
javascript
async function fetchDataWithRetry(url, retries = 3) {
  for (let attempt = 1; attempt <= retries; attempt++) {
    try {
      const response = await fetch(url);
      if (!response.ok) {
        throw new Error(`Attempt ${attempt}: Network response was not ok`);
      }
      const data = await response.json();
      return data;
    } catch (error) {
      console.error(`Attempt ${attempt} failed:`, error);
      if (attempt === retries) {
        throw new Error("All retry attempts failed");
      }
    }
  }
}

fetchDataWithRetry("https://api.example.com/data")
  .then((data) => console.log(data))
  .catch((error) => console.error(error));

Output (if all attempts fail):

Attempt 1 failed: Error: Attempt 1: Network response was not ok
Attempt 2 failed: Error: Attempt 2: Network response was not ok
Attempt 3 failed: Error: Attempt 3: Network response was not ok
Error: All retry attempts failed

Task 4: Using async Iterators ​

Shinobu's Advanced Maneuvers

Create an async generator that yields numbers from 1 to 5 with a delay of 500ms between each.

Hint:

Use async function* and await within the generator.

javascript
async function* numberGenerator() {
  // Your Code Here
}

(async () => {
  for await (const num of numberGenerator()) {
    console.log(num);
  }
})();
Answer
javascript
async function* numberGenerator() {
  for (let i = 1; i <= 5; i++) {
    await new Promise((resolve) => setTimeout(resolve, 500));
    yield i;
  }
}

(async () => {
  for await (const num of numberGenerator()) {
    console.log(num);
  }
})();

Output:

1
2
3
4
5

Extended Topics ​

Handling Race Conditions with Promise.race ​

Muzan's Unpredictable Nature

Promise.race returns the first settled Promise (fulfilled or rejected) among the provided Promises.

javascript
async function fetchWithTimeout(url, timeout) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeout);

  try {
    const response = await fetch(url, { signal: controller.signal });
    return await response.json();
  } catch (error) {
    if (error.name === "AbortError") {
      throw new Error("Fetch timed out");
    }
    throw error;
  } finally {
    clearTimeout(timeoutId);
  }
}

Promise.race([
  fetchWithTimeout("https://api.example.com/data", 3000),
  new Promise((_, reject) =>
    setTimeout(() => reject(new Error("Timeout")), 3000)
  ),
])
  .then((data) => console.log("Data:", data))
  .catch((error) => console.error("Error:", error));

Use Case:

Implementing a timeout for fetch requests to prevent hanging indefinitely.

Chaining Promises for Sequential Operations ​

Genya's Strategic Planning

Chain multiple Promises to perform operations in a specific order.

javascript
function firstOperation() {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("First operation completed");
      resolve(1);
    }, 1000);
  });
}

function secondOperation(value) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(`Second operation received value: ${value}`);
      resolve(value + 1);
    }, 1000);
  });
}

firstOperation()
  .then((result) => secondOperation(result))
  .then((finalResult) => console.log(`Final Result: ${finalResult}`))
  .catch((error) => console.error(error));

Output:

First operation completed
Second operation received value: 1
Final Result: 2

Common Pitfalls ​

Common Pitfalls

  • Ignoring Errors: Forgetting to handle rejected Promises can lead to unhandled promise rejections.
  • Overusing await: Using await unnecessarily can lead to sequential execution when parallel execution is possible.
  • Not Using try...catch: Failing to wrap await expressions in try...catch blocks can make error handling difficult.
  • Top-Level await Limitations: Not all environments support top-level await. Ensure compatibility or use it within modules.
  • Mixing async and .then(): While possible, mixing async/await with .then() can make the code less readable.

Conclusion ​

Mastering asynchronous JavaScript is like achieving Hashira levels in codingβ€”transforming your applications to be more efficient, responsive, and powerful.

  • async and await simplify asynchronous code, making it look synchronous.
  • Promises like Promise.all and Promise.allSettled allow handling multiple asynchronous operations effectively.
  • Error Handling ensures your application remains robust against failures.
  • Advanced Techniques like async iterators and Promise.race open doors to more sophisticated control over asynchronous flows.

Happy coding, and may your JavaScript prowess reach Demon Slayer levels! πŸ—‘οΈβœ¨


Additional Resources ​