R
Rishtaara
JavaScript Fundamentals
Lesson 20 of 28Article20 min

Asynchronous JavaScript

Old async code used nested callbacks. Many callbacks inside callbacks create "callback hell" — hard to read and debug.

The callback problem

Old async code used nested callbacks. Many callbacks inside callbacks create "callback hell" — hard to read and debug.

Real-life example: Calling a friend who calls another friend who calls a third person just to ask one question — too many steps in a chain.

Nested callbacks (hard to read)
getUser(1, (user) => {
  getCourses(user.id, (courses) => {
    getLesson(courses[0].id, (lesson) => {
      console.log(lesson.title); // deeply nested
    });
  });
});
// Promises and async/await fix this style

Promises

A Promise represents work that will finish later — success (resolve) or failure (reject). Chain .then and .catch instead of nesting.

Real-life example: A food delivery promise — "We will bring pizza in 30 minutes or give your money back." You wait without blocking the door.

Promise chain
function wait(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

wait(1000)
  .then(() => fetch("https://jsonplaceholder.typicode.com/posts/1"))
  .then((res) => res.json())
  .then((post) => console.log(post.title))
  .catch((err) => console.error(err));

async and await

async marks a function that returns a Promise. await pauses inside that function until a Promise finishes — code looks straight, like sync code.

Real-life example: await is like waiting at the counter for your coffee order number to be called — you do other things on your phone, but your turn comes in order.

async/await
async function loadPost() {
  const res = await fetch("https://jsonplaceholder.typicode.com/posts/1");
  const post = await res.json();
  return post.title;
}

loadPost().then(console.log);

fetch example

fetch is the modern way to get data from URLs in the browser. It returns a Promise.

Real-life example: fetch is like sending a waiter to the kitchen (server) to bring your dish (JSON data) back to your table (browser).

GET request with fetch
async function getCourses() {
  const response = await fetch("/api/courses");
  if (!response.ok) {
    throw new Error("Network error: " + response.status);
  }
  return response.json();
}

Error handling

Always wrap async code in try/catch. Network fails, servers return errors, and JSON can be invalid.

Real-life example: Ordering online — if payment fails, you see an error message instead of a blank screen. Good apps always plan for failure.

try/catch with async
async function safeLoad() {
  try {
    const res = await fetch("/api/data");
    if (!res.ok) throw new Error("HTTP " + res.status);
    return await res.json();
  } catch (error) {
    console.error("Could not load:", error.message);
    return { courses: [] }; // fallback
  }
}

Event loop — how async works

JavaScript runs one task at a time on the call stack. Slow jobs (timers, fetch) go to Web APIs. When done, callbacks join a queue and return to the stack.

Real-life example: A single cashier (call stack) serves one customer. While pizza bakes (Web API), others wait in line (queue) — the shop never freezes.

JavaScript event loop