06 - Async JavaScript

What is Asynchronous JavaScript?

JavaScript is single-threaded, but it can handle tasks that take time (like fetching data from an API) without blocking other code from running.

console.log("Start");

setTimeout(() => {
  console.log("This runs after 2 seconds");
}, 2000);

console.log("End");

// Output:
// Start
// End
// This runs after 2 seconds

Callbacks

The original way to handle async operations:

function fetchData(callback) {
  setTimeout(() => {
    const data = { id: 1, name: "Alice" };
    callback(data);
  }, 1000);
}

fetchData((result) => {
  console.log("Data received:", result);
});

// Real-world example — reading a file (Node.js)
const fs = require("fs");

fs.readFile("data.txt", "utf8", (error, data) => {
  if (error) {
    console.error("Error reading file:", error);
    return;
  }
  console.log("File contents:", data);
});

Callback Hell

Nested callbacks become hard to read:

// ❌ Callback hell — hard to read and maintain
getData((data) => {
  processData(data, (processed) => {
    saveData(processed, (saved) => {
      sendEmail(saved, (sent) => {
        console.log("All done!");
      });
    });
  });
});

Promises

A better way to handle async operations:

// Creating a promise
function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const success = true;

      if (success) {
        resolve({ id: 1, name: "Alice" });  // success
      } else {
        reject("Something went wrong");     // failure
      }
    }, 1000);
  });
}

// Using a promise
fetchData()
  .then((data) => {
    console.log("Success:", data);
  })
  .catch((error) => {
    console.error("Error:", error);
  });

Promise States

A promise can be in one of three states:

// pending — initial state, not fulfilled or rejected
// fulfilled — operation completed successfully
// rejected — operation failed

const promise = new Promise((resolve, reject) => {
  // Do async work here
  // Call resolve(value) on success
  // Call reject(error) on failure
});

Chaining Promises

function getUser(id) {
  return new Promise((resolve) => {
    setTimeout(() => resolve({ id, name: "Alice" }), 1000);
  });
}

function getPosts(userId) {
  return new Promise((resolve) => {
    setTimeout(() => resolve(["Post 1", "Post 2"]), 1000);
  });
}

// ✅ Much cleaner than callbacks
getUser(1)
  .then((user) => {
    console.log("User:", user);
    return getPosts(user.id);
  })
  .then((posts) => {
    console.log("Posts:", posts);
  })
  .catch((error) => {
    console.error("Error:", error);
  });

Promise Methods

// Promise.all — wait for all promises (fails if any fail)
const promise1 = fetch("https://api.example.com/users");
const promise2 = fetch("https://api.example.com/posts");

Promise.all([promise1, promise2])
  .then(([users, posts]) => {
    console.log("Both complete:", users, posts);
  })
  .catch((error) => {
    console.error("One failed:", error);
  });

// Promise.race — first one to finish wins
Promise.race([promise1, promise2])
  .then((result) => {
    console.log("First to finish:", result);
  });

// Promise.allSettled — wait for all, even if some fail
Promise.allSettled([promise1, promise2])
  .then((results) => {
    results.forEach((result) => {
      if (result.status === "fulfilled") {
        console.log("Success:", result.value);
      } else {
        console.log("Failed:", result.reason);
      }
    });
  });

// Promise.any — first success wins (ignores failures)
Promise.any([promise1, promise2])
  .then((result) => {
    console.log("First success:", result);
  })
  .catch(() => {
    console.log("All failed");
  });

Async/Await

Modern, cleaner syntax for promises:

// Function must be marked as async
async function fetchUser() {
  // await pauses execution until promise resolves
  const response = await fetch("https://api.example.com/user");
  const data = await response.json();
  return data;
}

// Using async function
fetchUser()
  .then((user) => console.log(user))
  .catch((error) => console.error(error));

// Or with await in another async function
async function main() {
  try {
    const user = await fetchUser();
    console.log("User:", user);
  } catch (error) {
    console.error("Error:", error);
  }
}

main();

Error Handling with Async/Await

async function fetchData() {
  try {
    const response = await fetch("https://api.example.com/data");

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const data = await response.json();
    return data;
  } catch (error) {
    console.error("Fetch failed:", error);
    throw error;  // re-throw if needed
  }
}

Sequential vs Parallel Execution

// ❌ Sequential — slow (3 seconds total)
async function slow() {
  const user = await fetchUser();      // 1 second
  const posts = await fetchPosts();    // 1 second
  const comments = await fetchComments(); // 1 second
  return { user, posts, comments };
}

// ✅ Parallel — fast (1 second total)
async function fast() {
  const [user, posts, comments] = await Promise.all([
    fetchUser(),      // all start at the same time
    fetchPosts(),
    fetchComments()
  ]);
  return { user, posts, comments };
}

// ✅ Parallel for independent tasks, sequential when one depends on another
async function smart() {
  const user = await fetchUser();           // 1 second
  const [posts, comments] = await Promise.all([
    fetchPosts(user.id),    // these run in parallel
    fetchComments(user.id)
  ]);
  return { user, posts, comments };
}

Fetch API

Making HTTP requests in modern JavaScript:

// GET request
async function getUsers() {
  const response = await fetch("https://api.example.com/users");
  const data = await response.json();
  return data;
}

// POST request
async function createUser(user) {
  const response = await fetch("https://api.example.com/users", {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify(user)
  });

  const data = await response.json();
  return data;
}

// Usage
createUser({ name: "Alice", email: "alice@example.com" })
  .then((user) => console.log("Created:", user))
  .catch((error) => console.error("Failed:", error));

// PUT request (update)
async function updateUser(id, updates) {
  const response = await fetch(`https://api.example.com/users/${id}`, {
    method: "PUT",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(updates)
  });

  return await response.json();
}

// DELETE request
async function deleteUser(id) {
  const response = await fetch(`https://api.example.com/users/${id}`, {
    method: "DELETE"
  });

  return response.ok;
}

Practical Example — Loading Data

<!DOCTYPE html>
<html>
<body>
  <div id="app">
    <h1>User List</h1>
    <button id="load">Load Users</button>
    <div id="loading" style="display: none;">Loading...</div>
    <ul id="users"></ul>
  </div>

  <script>
    const loadBtn = document.getElementById("load");
    const loading = document.getElementById("loading");
    const usersList = document.getElementById("users");

    loadBtn.addEventListener("click", loadUsers);

    async function loadUsers() {
      try {
        // Show loading
        loading.style.display = "block";
        usersList.innerHTML = "";

        // Fetch data
        const response = await fetch("https://jsonplaceholder.typicode.com/users");
        const users = await response.json();

        // Hide loading
        loading.style.display = "none";

        // Display users
        users.forEach((user) => {
          const li = document.createElement("li");
          li.textContent = `${user.name} (${user.email})`;
          usersList.appendChild(li);
        });
      } catch (error) {
        loading.style.display = "none";
        usersList.innerHTML = `<li style="color: red;">Error: ${error.message}</li>`;
      }
    }
  </script>
</body>
</html>

setTimeout and setInterval

// setTimeout — run once after delay
const timeoutId = setTimeout(() => {
  console.log("Runs after 2 seconds");
}, 2000);

// Cancel timeout
clearTimeout(timeoutId);

// setInterval — run repeatedly
const intervalId = setInterval(() => {
  console.log("Runs every second");
}, 1000);

// Cancel interval
clearInterval(intervalId);

// Practical example — countdown timer
let count = 10;
const countdown = setInterval(() => {
  console.log(count);
  count--;

  if (count < 0) {
    clearInterval(countdown);
    console.log("Done!");
  }
}, 1000);

Key Takeaways

  • JavaScript can handle async operations without blocking
  • Callbacks are the old way — can lead to callback hell
  • Promises provide a cleaner way to handle async operations
  • async/await is the modern, most readable syntax
  • Use try/catch with async/await for error handling
  • Promise.all runs promises in parallel for better performance
  • Fetch API is the modern way to make HTTP requests
  • Always handle errors in async code
  • Use await only inside async functions