Javascript
  • intro.
  • 1 - Getting started
  • 2 - Basics
  • 3 - Functions and Scope
  • 4 - Advanced Concepts
  • 5 - JavaScript in the Browser
  • 6 - JavaScript and Browser Storage
  • 7 - Asynchronous JavaScript
  • 8 - Design Patterns
  • 9 - Frameworks Overview
  • 10 - Testing and Debugging
Powered by GitBook
On this page

7 - Asynchronous JavaScript

Introduction to Asynchronous JavaScript

JavaScript is a single-threaded, non-blocking language, meaning it executes one line of code at a time. However, many operations, such as network requests or timers, need time to complete. Instead of blocking the entire application, JavaScript can handle these operations asynchronously. This chapter dives deep into how JavaScript manages asynchronous tasks using Callbacks, Promises, and Async/Await.

Callbacks

What are Callbacks?

  • Callback Function: A function passed as an argument to another function, to be called later after some operation completes.

  • How Callbacks Work: JavaScript functions are first-class objects, meaning they can be passed as parameters, stored in variables, and executed as required.

    • Example:

      function fetchData(callback) {
          setTimeout(() => {
              console.log("Fetching data...");
              callback("Data received");
          }, 2000);
      }
      
      function processData(data) {
          console.log(data);
      }
      
      fetchData(processData);  // Output after 2 seconds: "Fetching data...", "Data received"

Callback Hell

  • Callback Hell: When there are multiple nested callbacks, the code becomes complex and difficult to read, often referred to as "pyramid of doom".

    • Example:

      setTimeout(() => {
          console.log("Step 1");
          setTimeout(() => {
              console.log("Step 2");
              setTimeout(() => {
                  console.log("Step 3");
              }, 1000);
          }, 1000);
      }, 1000);
    • Problems: Difficult to maintain and error-prone.

Promises

What are Promises?

  • Promise: An object representing the eventual completion (or failure) of an asynchronous operation.

  • States of a Promise:

    1. Pending: Initial state, operation not completed.

    2. Fulfilled: Operation completed successfully.

    3. Rejected: Operation failed.

Creating and Using Promises

  • Creating a Promise:

    const myPromise = new Promise((resolve, reject) => {
        let success = true;
        if (success) {
            resolve("Promise fulfilled");
        } else {
            reject("Promise rejected");
        }
    });
  • Consuming Promises: Use .then() for fulfilled results, and .catch() for rejected cases.

    • Example:

      myPromise
          .then(result => {
              console.log(result);  // Output: "Promise fulfilled"
          })
          .catch(error => {
              console.error(error);
          });
  • finally(): Runs regardless of the promise being resolved or rejected.

    • Example:

      myPromise
          .then(result => console.log(result))
          .catch(error => console.error(error))
          .finally(() => console.log("Promise operation complete"));

Chaining Promises

  • Chaining: You can chain multiple .then() calls to handle sequences of asynchronous operations.

    • Example:

      const add = (a, b) => {
          return new Promise((resolve) => {
              setTimeout(() => {
                  resolve(a + b);
              }, 1000);
          });
      };
      
      add(5, 5)
          .then(result => {
              console.log(result);  // Output: 10
              return add(result, 10);
          })
          .then(result => {
              console.log(result);  // Output: 20
          })
          .catch(error => {
              console.error(error);
          });

Async/Await

What is Async/Await?

  • Async Functions: Functions declared with the async keyword. They always return a Promise.

  • Await: The await keyword is used to pause the execution of an async function until the Promise is resolved.

    • Benefit: Makes asynchronous code look and behave more like synchronous code, making it easier to read and debug.

    • Example:

      async function fetchData() {
          const data = new Promise((resolve) => {
              setTimeout(() => resolve("Data received"), 2000);
          });
          const result = await data;
          console.log(result);  // Output after 2 seconds: "Data received"
      }
      fetchData();

Error Handling with Async/Await

  • Use try...catch blocks to handle errors in async functions.

    • Example:

      async function fetchUserData() {
          try {
              const response = await fetch("https://api.example.com/user");
              const data = await response.json();
              console.log(data);
          } catch (error) {
              console.error("Error fetching data: ", error);
          }
      }
      fetchUserData();

Handling Multiple Promises

Promise.all()

  • Promise.all(): Accepts an array of Promises and resolves once all of them have been resolved.

    • If any Promise fails, the entire chain is rejected.

    • Example:

      const p1 = Promise.resolve(10);
      const p2 = new Promise((resolve) => setTimeout(() => resolve(20), 1000));
      const p3 = Promise.resolve(30);
      
      Promise.all([p1, p2, p3])
          .then(values => {
              console.log(values);  // Output: [10, 20, 30]
          })
          .catch(error => console.error(error));

Promise.race()

  • Promise.race(): Resolves or rejects as soon as any of the Promises in the array settle.

    • Example:

      const p1 = new Promise((resolve) => setTimeout(() => resolve("P1 resolved"), 2000));
      const p2 = new Promise((resolve) => setTimeout(() => resolve("P2 resolved"), 1000));
      
      Promise.race([p1, p2])
          .then(result => {
              console.log(result);  // Output: "P2 resolved"
          })
          .catch(error => console.error(error));

Asynchronous Iteration

for await...of Loop

  • for await...of: Used to iterate over async iterables, allowing you to handle multiple async operations sequentially.

    • Example:

      async function* generateNumbers() {
          yield new Promise(resolve => setTimeout(() => resolve(1), 1000));
          yield new Promise(resolve => setTimeout(() => resolve(2), 1000));
          yield new Promise(resolve => setTimeout(() => resolve(3), 1000));
      }
      
      (async () => {
          for await (let num of generateNumbers()) {
              console.log(num);  // Outputs: 1, then 2, then 3 (each after 1 second delay)
          }
      })();

Common Pitfalls and Best Practices

Pitfalls

  • Blocking Code: Avoid using synchronous code (like for loops with long iterations) inside async functions, as it defeats the purpose of asynchronous execution.

  • Overusing Promise.all(): When handling multiple Promises, using Promise.all() can cause failure if even one Promise fails.

Best Practices

  • Use Async/Await for Readability: Prefer async/await over .then() chaining for easier-to-read code.

  • Error Handling: Always use try...catch with async/await to manage potential failures.

  • Avoid Callback Hell: Migrate nested callbacks to Promises or async/await to improve code quality.

Summary

  • Callbacks: The traditional way of handling asynchronous tasks but can lead to callback hell.

  • Promises: Provide a more robust way to work with asynchronous code, reducing nesting issues.

  • Async/Await: Makes asynchronous code look synchronous, simplifying readability and debugging.

  • Promise Methods: Promise.all(), Promise.race(), and for await...of offer flexible ways to manage multiple asynchronous operations.

  • Best Practices: Use the right tool based on complexity and readability, manage errors effectively, and keep asynchronous code clean and maintainable.


Next Chapter: JavaScript Design Patterns

Previous6 - JavaScript and Browser StorageNext8 - Design Patterns

Last updated 6 months ago