Skip to content
JavaScript Async Programming Explained — Promises, Async/Await & Fetch

JavaScript Async Programming Explained — Promises, Async/Await & Fetch

DodaTech Updated Jun 6, 2026 8 min read

JavaScript is single-threaded — it can only do one thing at a time — yet web apps constantly wait for network requests, file reads, timers, and user input. Asynchronous programming is how JavaScript handles these waits without freezing the page.

What You’ll Learn

  • Why JavaScript needs async programming (the event loop)
  • Callbacks and the “callback hell” problem
  • Promises — creating, consuming, and chaining
  • Async/await — writing async code that reads like synchronous code
  • Fetch API for HTTP requests
  • Parallel vs sequential execution with Promise.all

Why Async Matters

Without async, clicking a button that fetches data would freeze the entire page until the response arrives. Imagine pressing “Download” in the Doda Browser and the whole browser freezes until the file finishes. Async lets the browser stay responsive while waiting. The Durga Antivirus Pro dashboard uses async to fetch threat updates, scan files in the background, and update the UI without blocking — all while you continue browsing.

Learning Path

    sequenceDiagram
  participant Main as Main Thread
  participant Task as Task Queue
  participant API as Web API
  Main->>API: fetch(url)
  Main->>Main: Continue executing
  API->>Task: Response ready
  Task->>Main: Process callback
  Note over Main: Callbacks → Promises → Async/Await
  
Prerequisites: You should understand JavaScript and JavaScript. Basic knowledge of REST API is helpful.

The Problem: Synchronous Code Blocks

console.log('Start');
const data = fetchData();  // Imaginary — takes 2 seconds
console.log('End');
console.log(data);          // We wait 2 seconds to see this!

In a synchronous world, fetchData() blocks everything for 2 seconds. The page freezes, buttons don’t respond, animations stop. Async solves this.

Callbacks (The Original Pattern)

A callback is a function passed as an argument to another function, to be executed later:

function fetchData(callback) {
  setTimeout(() => {
    callback('Data received');
  }, 1000);
}

fetchData((result) => {
  console.log(result);  // 'Data received' (after 1 second)
});

The problem — Callback Hell: When you need multiple sequential operations, callbacks nest endlessly:

fetchUser(1, (user) => {
  fetchPosts(user.id, (posts) => {
    fetchComments(posts[0].id, (comments) => {
      console.log(comments);  // Deeply nested — hard to read
    });
  });
});

This is called “callback hell” — code that grows to the right instead of downward.

Promises — A Better Pattern

A Promise represents a value that might not be available yet — like an IOU note. It’s either:

  • pending — waiting for the result
  • fulfilled — the operation succeeded
  • rejected — the operation failed
const promise = new Promise((resolve, reject) => {
  const success = true;
  
  if (success) {
    resolve('Operation completed');  // Fulfill the promise
  } else {
    reject('Operation failed');       // Reject the promise
  }
});

promise
  .then(result => console.log(result))    // 'Operation completed'
  .catch(error => console.error(error))   // Handle rejection
  .finally(() => console.log('Done'));    // Always runs

Analogy: A Promise is like ordering a pizza. You get a receipt (the Promise object). The pizza might be delivered (fulfilled) or you might get a call saying they’re out of dough (rejected). In either case, you’ll be notified.

Promise Chaining

fetchUser(1)
  .then(user => fetchPosts(user.id))
  .then(posts => fetchComments(posts[0].id))
  .then(comments => console.log(comments))
  .catch(err => console.error('Any error in the chain', err));

Each .then() returns a new Promise, allowing you to chain operations in a flat, readable structure — no more callback hell.

Static Promise Methods

const p1 = fetch('/api/users');
const p2 = fetch('/api/posts');

Promise.all([p1, p2])           // All must resolve (fast fail)
Promise.allSettled([p1, p2])    // All settle — never rejects
Promise.race([p1, p2])          // First to settle wins
Promise.any([p1, p2])           // First to fulfill wins

When to use each:

  • Promise.all — Multiple independent requests, need all results
  • Promise.allSettled — Need to know the result of each, even failures
  • Promise.race — Timeout implementation (fetch vs timeout timer)
  • Promise.any — Need the first successful result from multiple sources

Async/Await — Syntactic Sugar

async/await is syntactic sugar over Promises — it makes async code look synchronous:

async function loadData() {
  try {
    const user = await fetchUser(1);
    const posts = await fetchPosts(user.id);
    console.log(posts);
  } catch (error) {
    console.error('Failed to load:', error);
  }
}

loadData();

Line by line:

  • async function — This function now returns a Promise (even if you don’t create one).
  • await fetchUser(1) — Pause execution here until the Promise resolves. The result is assigned to user.
  • try/catch — If any awaited Promise rejects, execution jumps to catch.
  • loadData() — Call the async function. It runs immediately and returns a Promise.

Anticipating confusion: await does NOT block the entire program. It only pauses the current async function. Other code (like UI updates) continues running. Think of it like a pit stop in a race — your car pauses, but other cars keep going.

Sequential vs Parallel

// Sequential — slow (2 seconds total)
const a = await fetch('/api/a');
const b = await fetch('/api/b');

// Parallel — fast (1 second total)
const [a, b] = await Promise.all([
  fetch('/api/a'),
  fetch('/api/b')
]);

Use Promise.all for independent requests. Use separate await statements when the second request depends on the first.

Fetch API

The modern way to make HTTP requests:

// GET request
async function getUsers() {
  try {
    const response = await fetch('https://api.example.com/users');
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Fetch failed:', error);
    return [];
  }
}

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

Critical: Always check response.ok. Fetch only rejects on network errors, not HTTP errors (like 404 or 500). You must check response.ok or response.status yourself.

Common Mistakes

1. Forgetting await inside async functions

async function load() {
  const data = fetch('/api/data');  // Returns Promise, not data!
  console.log(data);                 // Promise { <pending> }
}

Fix: Always use await to resolve Promises.

2. Not catching promise rejections

async function load() {
  const data = await fetch('/api/data');
  // If fetch fails, this throws an unhandled rejection
}

Fix: Wrap in try/catch or chain .catch().

3. Sequential promises that should be parallel

const a = await fetch('/api/a');
const b = await fetch('/api/b');  // Waits for a to finish first

Fix: Use Promise.all for independent requests.

4. Not checking response.ok with fetch

const res = await fetch('/api/data');
const data = await res.json();  // Throws if response is not JSON

Fix: Check res.ok before parsing.

5. Mixing .then and await in the same function

Pick one style. Using both is confusing and error-prone.

Practice Questions

  1. What’s the difference between Promise.all and Promise.allSettled? Promise.all rejects fast (if any rejects). Promise.allSettled waits for all to settle and never rejects.

  2. What does await do in an async function? It pauses execution until the Promise resolves, then returns the resolved value.

  3. How does the event loop handle async operations? Synchronous code runs first, then microtasks (Promises), then macrotasks (setTimeout, I/O).

  4. Why should you check response.ok with fetch? Fetch only rejects on network errors, not HTTP error status codes.

Challenge: Build a function fetchWithTimeout(url, ms) that fetches a URL but rejects if the request takes longer than ms milliseconds. Use Promise.race with a timeout promise.

FAQ

What is the difference between promise .then() and async/await?
Both work with promises. async/await is syntactic sugar that reads like synchronous code. Use .then() for simple chains, async/await for complex logic.
What is callback hell?
Nested callbacks where each level is indented further, making code hard to read and debug. Promises and async/await solve this.
What does Promise.all do?
Runs multiple promises in parallel and resolves when all succeed. If any rejects, the entire promise rejects immediately.
What is the event loop?
JavaScript’s mechanism for handling async operations. The main thread executes synchronous code, then processes microtasks (promises) then macrotasks (setTimeout, I/O).
What is the difference between setTimeout and setInterval?
setTimeout runs once after a delay. setInterval runs repeatedly every interval.
How do I handle errors with async/await?
Wrap the code in try/catch. Unhandled promise rejections cause uncaught exceptions.

Try It Yourself

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Async Playground</title>
  <style>
    body { font-family: Arial, sans-serif; padding: 2rem; }
    pre { background: #f4f4f4; padding: 1rem; border-radius: 5px; }
    button { margin-right: 0.5rem; padding: 0.5rem 1rem; }
  </style>
</head>
<body>
  <h1>Async JavaScript Sandbox</h1>
  <button id="btnLoad">Simulate API Call</button>
  <button id="btnParallel">Parallel Calls</button>
  <pre id="output">Click a button to see async in action.</pre>

  <script>
    const out = document.getElementById('output');

    function delay(ms, value) {
      return new Promise(resolve => setTimeout(() => resolve(value), ms));
    }

    document.getElementById('btnLoad').addEventListener('click', async () => {
      out.textContent = 'Loading...';
      const result = await delay(1500, 'Data loaded successfully!');
      out.textContent = result;
    });

    document.getElementById('btnParallel').addEventListener('click', async () => {
      out.textContent = 'Fetching in parallel...\n';
      const start = Date.now();
      
      const [a, b] = await Promise.all([
        delay(1000, 'Result A'),
        delay(1500, 'Result B')
      ]);
      
      const elapsed = ((Date.now() - start) / 1000).toFixed(2);
      out.textContent += `${a}\n${b}\nCompleted in ${elapsed}s (not 2.5s!)`;
    });
  </script>
</body>
</html>

What’s Next

Now that you understand async JavaScript, learn about modules and advanced topics:

LessonDescription
JavaScript HomeBack to the JavaScript hub
https://tutorials.dodatech.com/programming-languages/javascript/js-modules/Modules & Error Handling
https://tutorials.dodatech.com/programming-languages/javascript/js-json/JSON — parse API responses
https://tutorials.dodatech.com/programming-languages/javascript/js-advanced/Advanced JavaScript
Node.js AsyncServer-side async with Node.js

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

What’s Next

Congratulations on completing this Js Async tutorial! Here’s where to go from here:

  • Practice daily — Consistency is more important than long study sessions
  • Build a project — Apply what you learned by building something real
  • Explore related topics — Check out other tutorials in the same category
  • Join the community — Discuss with other learners and share your progress

Remember: every expert was once a beginner. Keep coding!

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro