Asynchronous JavaScript

Introduction to Asynchronous JavaScript

  1. 1. Asynchronous programming allows non-blocking operations in JavaScript
  2. 2. In synchronous programming, tasks are executed one after another, blocking the execution until each task completes
  3. 3. Why do we need Asynchronous JavaScript?
    • - To handle time-consuming operations without blocking the main thread
    • - To improve application performance and user experience
    • - To handle operations like:
      • 1.Fetching data from servers (API calls)
      • 2.Reading files (Node.js)
      • 3.Complex calculations
      • 4.Database operations (Node.js)

Ways to Handle Asynchronous Operations

  1. Callbacks

    Callbacks are functions passed as arguments to another function that will be executed later.

    // Example of callback
    function fetchData(callback) {
      setTimeout(() => {
        const data = { id: 1, name: 'John' };
        callback(data);
      }, 2000);
    }
    
    fetchData((result) => {
      console.log(result); // Runs after 2 seconds
    });
  2. Promises

    Promises are objects representing the eventual completion (or failure) of an asynchronous operation.

    // Example of Promise
    const fetchUserData = new Promise((resolve, reject) => {
      setTimeout(() => {
        const user = { id: 1, name: 'John' };
        resolve(user);
        // reject('Error fetching user');
      }, 2000);
    });
    
    fetchUserData
      .then(user => console.log(user))
      .catch(error => console.error(error));
  3. Async/Await

    Async/await makes complicated code simpler to write and understand. It helps developers write code that needs to wait for things (like getting data from the internet) in a way (that's easier to read).

    // Example of async/await
    async function fetchUser() {
      try {
        const response = await fetch('https://api.example.com/user');
        const user = await response.json();
        console.log(user);
      } catch (error) {
        console.error('Error:', error);
      }
    }
  4. Callback Hell

    Callback Hell (also known as Pyramid of Doom) occurs when we have multiple nested callbacks, making the code difficult to read and maintain.

    // Example of Callback Hell
    fetchUserData((user) => {
      console.log('Fetched user');
      getUserPosts(user.id, (posts) => {
        console.log('Fetched posts');
        getPostComments(posts[0].id, (comments) => {
          console.log('Fetched comments');
          getCommentAuthor(comments[0].id, (author) => {
            console.log('Fetched author');
            // Code becomes deeply nested and hard to read
          }, (error) => {
            console.error('Error fetching author:', error);
          });
        }, (error) => {
          console.error('Error fetching comments:', error);
        });
      }, (error) => {
        console.error('Error fetching posts:', error);
      });
    }, (error) => {
      console.error('Error fetching user:', error);
    });
  5. Solving Callback Hell

    We can solve callback hell using Promises or async/await:

    // Using Promises
    fetchUserData()
      .then(user => {
        console.log('Fetched user');
        return getUserPosts(user.id);
      })
      .then(posts => {
        console.log('Fetched posts');
        return getPostComments(posts[0].id);
      })
      .then(comments => {
        console.log('Fetched comments');
        return getCommentAuthor(comments[0].id);
      })
      .then(author => {
        console.log('Fetched author');
      })
      .catch(error => {
        console.error('Error:', error);
      });
    
    // Using async/await (even cleaner)
    async function fetchUserDataChain() {
      try {
        const user = await fetchUserData();
        console.log('Fetched user');
        
        const posts = await getUserPosts(user.id);
        console.log('Fetched posts');
        
        const comments = await getPostComments(posts[0].id);
        console.log('Fetched comments');
        
        const author = await getCommentAuthor(comments[0].id);
        console.log('Fetched author');
      } catch (error) {
        console.error('Error:', error);
      }
    }
  6. Problems with Callback Hell:
    • Code becomes difficult to read and maintain
    • Error handling becomes complicated
    • Debugging becomes challenging
    • Code becomes less reusable

Common Asynchronous Operations

  1. 1. setTimeout and setInterval
    // setTimeout - runs once after delay
    setTimeout(() => {
      console.log('Runs after 2 seconds');
    }, 2000);
    
    // setInterval - runs repeatedly
    const timer = setInterval(() => {
      console.log('Runs every 1 second');
    }, 1000);
    
    // Clear interval
    clearInterval(timer);
  2. 2. API Calls using fetch
    // Using fetch with async/await
    async function getUsers() {
      try {
        const response = await fetch('https://api.example.com/users');
        const users = await response.json();
        return users;
      } catch (error) {
        console.error('Error fetching users:', error);
      }
    }

Best Practices

  1. 1. Always handle errors in asynchronous operations using try-catch or .catch()
  2. 2. Avoid callback hell by using Promises or async/await
  3. 3. Use Promise.all() when dealing with multiple independent promises
  4. Note: Modern JavaScript primarily uses Promises and async/await for handling asynchronous operations as they provide better readability and error handling compared to callbacks.