JavaScript Roadmap — Day 19: Promises & Async/Await — Deep Dive

 

JavaScript · Day 19 of 180 · Beginner Phase

Promises & Async/Await — Deep Dive

Async JavaScript is the bridge between your code and the real world — APIs, databases, file systems. Master Promises and Async/Await and you can build applications that talk to servers, fetch live data, and handle real-world operations.

Day 19 / 180
Beginner Phase
🕐 16 min read
💻 8 code examples
🎯 3 practice tasks

Here is the code that confused me for months:

The code that confused every beginner
const response = await fetch("https://api.example.com/users");
const data     = await response.json();
console.log(data); // works! but WHY?

// Meanwhile this doesn't work:
const res  = fetch("https://api.example.com/users");
console.log(res); // Promise { <pending> } — not the data!

Every beginner hits this wall. The data is not there. The Promise is pending. Nothing makes sense. Today I am going to explain exactly what is happening — from the very beginning. By the end of this lesson async JavaScript will be completely clear and you will never be confused by it again.

1. The Async Problem — Why JavaScript Needs This

JavaScript is single-threaded — it can only do one thing at a time. When it makes a network request — it cannot stop and wait because that would freeze the entire page. So instead JavaScript sends the request and continues running other code. This is the async problem:

The Async Problem
// JavaScript runs line by line — top to bottom
console.log("1 — start");

// setTimeout simulates a delayed operation (like API call)
setTimeout(() => {
  console.log("2 — delayed (1 second)");
}, 1000);

console.log("3 — end");

// Output ORDER:
// 1 — start
// 3 — end       ← runs BEFORE the timeout!
// 2 — delayed   ← runs after 1 second

// This is exactly what happens with API calls
const data = fetch("https://api.example.com"); // starts request
console.log(data); // Promise {pending} — data not here yet!
// response arrives 200ms later — too late

💡 The mental model: Think of ordering food at a restaurant. You place your order (API request) and the waiter says "I will bring it when it is ready" (Promise). You do not stand frozen at the counter waiting — you sit down and do other things (JavaScript keeps running). When the food is ready the waiter brings it to you (callback/await resolves).

2. Callbacks — The Original Solution (and its Problem)

Before Promises — the solution was callbacks. A callback is a function you pass to another function to call when it is done. It works but creates a nightmare when you have multiple async operations depending on each other:

Callbacks — and Callback Hell
// Simple callback — works fine
function fetchUser(id, callback) {
  setTimeout(() => {
    callback({ id, name: "Waheed" });
  }, 1000);
}

fetchUser(1, (user) => {
  console.log(user.name); // "Waheed"
});

// ❌ Callback Hell — multiple dependent operations
getUser(1, (user) => {
  getPosts(user.id, (posts) => {
    getComments(posts[0].id, (comments) => {
      getLikes(comments[0].id, (likes) => {
        console.log(likes); // 4 levels deep — impossible to read!
      });
    });
  });
});
// This is "Callback Hell" or "Pyramid of Doom"
// Promises were created to solve this exact problem

3. Promises — The Solution

A Promise is an object that represents a value that will be available in the future. It has three states — pending, fulfilled, and rejected. You create one with new Promise() and resolve or reject it when the operation completes:

JavaScript — Creating & Using Promises
// Create a Promise
const myPromise = new Promise((resolve, reject) => {
  // Do async work here
  const success = true;

  if (success) {
    resolve("Data loaded!");   // fulfilled ✅
  } else {
    reject("Something failed!"); // rejected ❌
  }
});

// Consume with .then() and .catch()
myPromise
  .then(data  => console.log(data))   // "Data loaded!"
  .catch(error => console.log(error)); // only if rejected

// Real example — simulated API call
function fetchUser(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id > 0) {
        resolve({ id, name: "Waheed", city: "Faisalabad" });
      } else {
        reject(new Error("Invalid user ID"));
      }
    }, 1000);
  });
}

fetchUser(1)
  .then(user  => console.log(`Hello ${user.name}!`))
  .catch(err  => console.log(`Error: ${err.message}`))
  .finally(() => console.log("Always runs"));

4. Promise Chaining — Solving Callback Hell

Promises can be chained with .then(). Each .then() returns a new Promise — so you can chain dependent async operations in a flat, readable way instead of nesting:

Promise Chaining
// Chain vs Callback Hell comparison

// ❌ Callback Hell
getUser(1, (user) => {
  getPosts(user.id, (posts) => {
    getComments(posts[0].id, (comments) => {
      console.log(comments); // deeply nested
    });
  });
});

// ✅ Promise Chain — flat and readable
getUser(1)
  .then(user    => getPosts(user.id))
  .then(posts   => getComments(posts[0].id))
  .then(comments => console.log(comments))
  .catch(err    => console.log(`Error: ${err.message}`));

// Real world — fetch API chain
fetch("https://jsonplaceholder.typicode.com/users/1")
  .then(response  => {
    if (!response.ok) throw new Error("HTTP error!");
    return response.json(); // returns another Promise
  })
  .then(user      => console.log(user.name))
  .catch(error    => console.log(`Failed: ${error.message}`))
  .finally(()   => console.log("Request complete"));

5. Async/Await — Modern & Clean 🔥

async/await is syntactic sugar built on top of Promises. It makes async code look and behave like synchronous code — easy to read, easy to debug. The await keyword pauses execution inside an async function until the Promise resolves:

Async/Await
// async function — always returns a Promise
async function getUser(id) {
  const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
  const user     = await response.json();
  return user;
}

// Arrow function syntax
const getPost = async (id) => {
  const res  = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
  const post = await res.json();
  return post;
};

// await pauses execution until Promise resolves
async function loadData() {
  console.log("1 — starting");
  const user = await getUser(1);  // pauses here
  console.log("2 — user loaded:", user.name);
  const post = await getPost(1);  // pauses here
  console.log("3 — post loaded:", post.title);
  console.log("4 — done!");
}

loadData();
// Output in ORDER: 1 → 2 → 3 → 4 ✅

6. Error Handling — try/catch with Async

With async/await you handle errors using try/catch — exactly like synchronous code. This is much cleaner than chaining .catch() and makes error handling intuitive:

Error Handling with try/catch
// Always wrap await in try/catch
async function fetchData(url) {
  try {
    const response = await fetch(url);

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

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

  } catch (error) {
    // Handles network errors AND thrown errors
    console.error(`Failed: ${error.message}`);
    return null; // return null instead of crashing

  } finally {
    // Always runs — good for loading states
    console.log("Loading complete");
  }
}

// Real world pattern — loading state
async function loadUsers() {
  const btn = document.querySelector("#loadBtn");
  btn.textContent = "Loading...";
  btn.disabled = true;

  try {
    const users = await fetchData("https://jsonplaceholder.typicode.com/users");
    console.log(`Loaded ${users.length} users`);
  } catch (err) {
    console.log("Error loading users");
  } finally {
    btn.textContent = "Load Users";
    btn.disabled = false;
  }
}
Pro Tip #1 — Always check response.ok
fetch() only rejects on network failure — not on HTTP errors like 404 or 500. Always check response.ok or response.status before calling response.json(). Many beginners miss this and wonder why their error handling never fires.
Pro Tip #2 — async functions always return Promises
Even if you return a plain value from an async function — it is automatically wrapped in a resolved Promise. This means you can always await an async function or chain .then() on it. Understanding this removes a lot of confusion.
Pro Tip #3 — finally for cleanup
The finally block always runs — whether the request succeeded or failed. Use it to reset loading states, hide spinners, enable buttons, or close connections. This pattern is used in every production application.

7. Promise Methods — Parallel & Advanced Patterns

JavaScript provides several static Promise methods for handling multiple async operations at once. Promise.all() is the most important — it runs multiple requests in parallel and waits for all of them:

Promise Static Methods
// ── Promise.all() — run in PARALLEL ──
async function loadDashboard() {
  try {
    // ❌ Slow — sequential (3 seconds total)
    const user  = await getUser(1);   // 1s
    const posts = await getPosts(1);  // 1s
    const stats = await getStats(1);  // 1s = 3s total

    // ✅ Fast — parallel (1 second total)
    const [user2, posts2, stats2] = await Promise.all([
      getUser(1),
      getPosts(1),
      getStats(1)
    ]); // all run at same time → 1s total

  } catch (err) {
    // If ANY promise fails — catch fires
    console.log(`One failed: ${err.message}`);
  }
}

// ── Promise.allSettled() — all results even if some fail ──
const results = await Promise.allSettled([
  getUser(1),
  getUser(-1), // this will fail
  getUser(3)
]);
results.forEach(r => {
  if (r.status === "fulfilled") console.log(r.value);
  if (r.status === "rejected")  console.log(r.reason.message);
});

// ── Promise.race() — first to finish wins ──
const fastest = await Promise.race([
  fetchFromServer1(),
  fetchFromServer2(),
  fetchFromServer3()
]); // whichever responds first

// ── Promise.resolve() / Promise.reject() ──
const resolved = await Promise.resolve("immediate value");
console.log(resolved); // "immediate value"

8. Try It Yourself — Live API Playground

This playground makes real API calls to JSONPlaceholder — a free fake REST API. Click Run and watch real async JavaScript in action!

✏️ Live API Playground — Real fetch() calls
HTML
JAVASCRIPT
LIVE PREVIEW
💡 Makes real API calls to jsonplaceholder.typicode.com!

9. Practice Tasks

Task 1 — Easy: User Profile Fetcher

Write an async function getUserProfile(id) that fetches from https://jsonplaceholder.typicode.com/users/{id}. Check response.ok. Return an object with name, email, and city only. Use try/catch with meaningful error messages. Test with valid id (1-10) and invalid id (99).

Task 2 — Medium: Posts Dashboard

Build a dashboard loader using Promise.all(). Fetch users AND posts at the same time. When both are loaded — find each user's posts and display a summary: "User: {name} has {X} posts". Use Promise.allSettled() so one failure does not break everything. Log the time taken.

Task 3 — Hard: Retry Mechanism

Build a fetchWithRetry(url, maxRetries) function. If the request fails — wait 1 second and try again — up to maxRetries times. Use a loop with async/await inside. Track attempt number. If all retries fail — throw a final error with "Failed after X attempts". Show loading state for each attempt. This is a real production pattern used in every serious application.

Next Lesson
Day 20 — Fetch API & REST APIs
View Full Roadmap →
Enjoying this roadmap?
Follow Muhammad Waheed Asghar for daily JavaScript tips and updates!

Popular Posts