Notifications

No notifications

/Phase 2

Async JavaScript

Async JavaScript β€” Handling Time & Network

JavaScript is single-threaded β€” it can only do one thing at a time. Asynchronous programming lets you start long-running operations (network requests, timers) without blocking the main thread.

The Evolution of Async

Callbacks (ES5)  β†’  Promises (ES6)  β†’  async/await (ES2017)

Callbacks

A callback is a function passed as an argument to be called later:

setTimeout(() => {
  console.log("1 second later");
}, 1000);

Problem: Nested callbacks create callback hell (pyramid of doom).

Promises

A Promise represents a value that will be available in the future:

StateMeaning
PendingOperation in progress
FulfilledCompleted successfully β†’ .then()
RejectedFailed β†’ .catch()

fetch("/api/users")
  .then(res => res.json())
  .then(data => console.log(data))
  .catch(err => console.error(err))
  .finally(() => console.log("Done"));

async / await

Syntactic sugar over Promises β€” makes async code read like synchronous code:

async function getUsers() {
  try {
    const res = await fetch("/api/users");
    const data = await res.json();
    return data;
  } catch (err) {
    console.error("Failed:", err);
  }
}

Parallel Execution

MethodBehavior
Promise.all()Wait for ALL to resolve (fails if any reject)
Promise.allSettled()Wait for ALL (never fails)
Promise.race()Resolve/reject with first settled
Promise.any()Resolve with first fulfilled

The Event Loop

Call Stack  β†’  Microtask Queue (Promises)  β†’  Macrotask Queue (setTimeout)

Microtasks (Promises) always run before macrotasks (timers, I/O).

On this page

Detailed Theory

Why "Async" Even Exists

JavaScript runs on a single thread. That means only one piece of code can execute at a time. If you ask the network for data and *wait*, the entire page would freeze β€” buttons wouldn't click, animations would stop. That's unacceptable.

The solution: async work runs in the background, and the language gives you tools to react when it finishes. That's all "async" really means β€” "don't block the page while you're waiting."

Mental Model: The Three Tools

JavaScript has had three generations of async tools, each cleaner than the last:

1. Callbacks β€” pass a function to be called later. (Old, leads to "callback hell".) 2. Promises β€” an object that represents "a value that's coming soon". 3. async / await β€” modern syntax that lets you write Promise code that *looks* synchronous.

You'll mostly write async/await today, but you must understand Promises underneath.

Callbacks β€” The Original

setTimeout(() => {
  console.log("Runs after 1 second");
}, 1000);

You hand setTimeout a function. It calls it later. Simple β€” but when one async step depends on the previous, you get nesting nightmares ("pyramid of doom").

Promises β€” The Big Upgrade

A Promise is an object with one of three states:

  • pending β€” work is in flight
  • fulfilled β€” finished successfully (it has a value)
  • rejected β€” failed (it has an error)
fetch("/api/users")
  .then((res) => res.json())     // runs on success
  .then((users) => console.log(users))
  .catch((err) => console.error(err)) // runs on any failure
  .finally(() => hideSpinner());      // runs no matter what

Each .then returns *another* Promise β€” that's why you can chain them flat instead of nesting.

async / await β€” The Modern Way

async/await is just sugar over Promises. Mark a function async and you can await Promises inside it as if they were plain values:

async function loadUsers() {
  const res = await fetch("/api/users");
  const users = await res.json();
  return users;
}

loadUsers().then(console.log);

Two rules:

1. await only works inside an async function (or top-level in modules). 2. An async function always returns a Promise β€” even if you return a plain number.

Error Handling with try / catch

async function load() {
  try {
    const res = await fetch("/api/users");
    if (!res.ok) throw new Error(HTTP ${res.status});
    const users = await res.json();
    return users;
  } catch (err) {
    console.error("Failed:", err);
    showToast("Couldn't load users");
  } finally {
    hideSpinner();
  }
}

Important: fetch does not reject on HTTP 404 or 500 β€” it only rejects on network failure. You have to check res.ok yourself.

The fetch API in 90 Seconds

// GET
const res = await fetch("/api/users");
const users = await res.json();

// POST const res = await fetch("/api/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: "Asha" }), });

Beginner Mistakes to Skip

1. Forgetting await β€” const u = fetch(...) gives you a *Promise object*, not data. 2. Sequential when you could be parallel β€” three independent await fetches in a row are wasted seconds. Use Promise.all. 3. Forgetting !res.ok β€” fetch happily resolves on a 500 error. 4. Mixing async with .forEach β€” array.forEach(async (x) => await thing(x)) does *not* await anything. Use for...of or Promise.all(map(...)). 5. Unhandled rejections β€” every Promise chain should end in .catch or live inside try/catch.

Intermediate: Running Things in Parallel

If three requests don't depend on each other, fire them at the same time:

// Slow β€” 3 sequential requests
const a = await fetch("/a");
const b = await fetch("/b");
const c = await fetch("/c");

// Fast β€” all 3 in flight at once const [a, b, c] = await Promise.all([ fetch("/a"), fetch("/b"), fetch("/c"), ]);

The Promise Combinators

MethodBehaviour
Promise.allWaits for all; rejects fast if any fail
Promise.allSettledWaits for all; never rejects β€” gives {status, value/reason}
Promise.raceResolves/rejects with whichever finishes first
Promise.anyResolves with the first success; rejects only if *all* fail

Intermediate: The Event Loop

This is *the* interview question. Predict the output:

console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
console.log("4");
// β†’ 1, 4, 3, 2

Order of operations:

1. Synchronous code runs first on the call stack. 2. After the stack empties, microtasks drain (Promises, queueMicrotask). 3. Then one macrotask runs (setTimeout, I/O). 4. Microtasks drain again, then next macrotask… repeat.

Microtasks always beat setTimeout(..., 0).

Intermediate: Cancelling Async Work

A user clicks "Search", types a new query, and now there are two in-flight requests racing each other. Cancel the first one:

const ctrl = new AbortController();

fetch("/api/search?q=foo", { signal: ctrl.signal }) .then(/* … */) .catch((err) => { if (err.name === "AbortError") return; // expected throw err; });

// Later β€” abandon it: ctrl.abort();

The AbortController is also accepted by addEventListener (see the Events topic). One pattern, many uses.

Intermediate: Timeouts

Add a timeout to any fetch using AbortSignal.timeout:

const res = await fetch("/api/slow", {
  signal: AbortSignal.timeout(5000), // give up after 5 s
});

Advanced: Top-Level await

Inside ES Modules you can await directly at the top of a file:

// config.js (a module)
const res = await fetch("/config.json");
export const config = await res.json();

The module's importers wait until that Promise resolves. Use sparingly β€” it can delay your whole bundle.

Advanced: Async Iterators & for await…of

Stream data piece by piece:

const res = await fetch("/big-file");
for await (const chunk of res.body) {
  process(chunk); // each chunk arrives as it's downloaded
}

Perfect for log streams, AI token-by-token responses, or massive CSVs.

Advanced: Avoiding the Blocked Event Loop

Even async code blocks the page if your *synchronous* JavaScript runs too long (heavy loops, big JSON parsing). Two escape hatches:

1. Yield to the browser with await new Promise(r => setTimeout(r)) between iterations. 2. Move heavy work to a Web Worker β€” a separate thread with its own event loop, communicating via postMessage. Pure CPU code (parsing, image processing, ML) belongs there.

Advanced: Promise Anti-Patterns

// ❌ The "Promise constructor anti-pattern"
function get() {
  return new Promise((resolve) => {
    fetch("/x").then((r) => resolve(r));
  });
}
// βœ… Just return the existing Promise:
function get() {
  return fetch("/x");
}

// ❌ Swallowed errors async function load() { try { await fetchData(); } catch {} // silently broken forever } // βœ… At least log it catch (e) { console.error(e); }

Practice Path

1. Convert a chain of three .then calls into one async/await function. 2. Take three sequential await fetches and parallelise them with Promise.all. Time the difference. 3. Build a search input that cancels the previous request whenever the user types again, using AbortController. 4. Predict the output of mixed setTimeout + Promise.then + queueMicrotask snippets, then run them and confirm.