WEEK 9 · ISLAND OF TIME

Asynchronous JavaScript

JavaScript does one thing at a time — yet the page never freezes. This is how.

This week event loop setTimeout callbacks Promises async / await Fetch API

0 Week at a glance

callbacks Promises async/await fetch. Each generation fixes the last one's problems.
  • Async programming — start slow work now, handle the result later, never freeze the page.
  • Event loop — how one single thread fakes doing many things at once.
  • Three tools — callbacks (old) · Promises (ES6) · async/await (ES2017, built on Promises).
  • Fetch API — real data from real servers, returned as a Promise.

Every example is live. Press Run to execute — or click into the code and edit it. Flip auto-run (top-right of each example) to re-run automatically as you type.

1 Synchronous vs. asynchronous

Key terms synchronous = blocking asynchronous = non-blocking event-driven
One barista. Sync: serve one customer start-to-finish. Async: start the machine, serve the next customer while it brews.

Synchronous — blocking

  • Each line finishes completely before the next starts. Order is guaranteed.
  • Cost: while it runs, the page is frozen — no clicks, no scroll, no animation.
synchronous.js
console.log("Start");

for (let i = 0; i < 5; i++) {
  console.log("Processing...");
}

console.log("End");

Asynchronous — event-driven

  • Register a function to run later, when something happens (click, timer, data arrives).
  • Registration is instant — the rest of the code keeps going.
event-driven.html
<button id="btn">Click me</button>
<script>
  const button = document.getElementById("btn");

  button.addEventListener("click", function () {
    alert("Button was clicked!");   // runs LATER — only when clicked
  });

  console.log("Page loaded");        // runs NOW
</script>

Why we need it

BenefitMeaning
Better UXPage stays responsive while data loads.
Non-blocking I/ONetwork / file work runs in the background.
PerformanceMany slow operations in flight at once.
ScalabilitySame model powers Node.js servers with thousands of connections.

2 How JavaScript executes

Key terms single thread call stack Web APIs callback queue event loop
JS has one thread. The browser does the waiting. The event loop brings results back — when the stack is empty.
  • Call stack — where your functions run, one at a time.
  • Web APIs — browser helpers (timers, network, events) working outside the JS thread.
  • Callback queue — finished background work waits here.
  • Event loop — moves queued callbacks onto the stack only when it's empty.

Watch it happen

event-loop-demo.js
console.log("Start");setTimeout(() => {  console.log("Processing...");}, 2000);console.log("End");
Press Play to start the simulation.
📚 Call stack (JS thread)
🌐 Web APIs (browser)
⏳ Callback queue
🖥️ Console

Exam favourite: a callback never interrupts running code. Even setTimeout(fn, 0) waits for the current code to finish — the delay is a minimum, not a guarantee.

3 setTimeout & timers

Key terms setTimeout(fn, ms) callback 1000 ms = 1 s
syntax
setTimeout(callbackFunction, delay, param1, param2, ...);
// callbackFunction : code to run after the delay
// delay            : wait time in milliseconds
// params           : optional arguments for the callback

The most important example of the week

Predict the order — then Run.
timers.js
console.log("Before setTimeout");

const secondInMilliseconds = 1000;
setTimeout(() => {
  console.log("A second has passed");
}, secondInMilliseconds);

console.log("After setTimeout");
  • JS does not stop at setTimeout — the timer goes to the browser, the code goes on.
  • This one behaviour explains everything else this week.

4 Callbacks — the original technique

Key terms function as argument called back later no parentheses!

Callback = a function passed as an argument to another function, to be called back later. Used for HTTP requests, timers, user events, file operations.

callback-basics.js
const doSomething = (cb) => {
  console.log("Doing something...");
  cb();                      // "calling back" the function we were given
};

const nextStep = () => {
  console.log("Callback called");
};

doSomething(nextStep);        // pass the FUNCTION itself — no parentheses!

Classic mistake: doSomething(nextStep()) calls it now. doSomething(nextStep) passes it to call later.

Callbacks for async work

Async functions can't return the result — they deliver it through the callback.
async-callback.js
function fetchData(callback) {
  console.log("Fetching data...");
  setTimeout(() => {
    const data = { name: "Abul Qasim", role: "Professor" };
    callback(data);              // deliver the result via the callback
  }, 2000);
}

function processData(data) {
  console.log("Data received:", data);
}

fetchData(processData);          // pass processData as the callback

5 Callback hell

Key terms pyramid of doom nesting error handling at every level
Steps that depend on each other must nest. Add error checks, and the code slides off the screen.
getUser(1, function (err, user) { getPosts(user.id, function (err, posts) { getComments(posts[0].id, function (err, comments) { getAuthor(comments[0].by, function (err, author) { sendThanks(author, function (err, status) { // ...you are now 5 levels deep 💀 }); }); }); }); });

The real thing, with the error checks every level needs. Try to follow it — that's the point:

callback-hell.js
// Five fake "API" functions — each takes (input, callback(err, result))
function getUser(id, cb)       { setTimeout(() => cb(null, { id: id, name: "Dinn" }), 500); }
function getPosts(userId, cb)  { setTimeout(() => cb(null, [{ id: 7, title: "Post 1" }]), 500); }
function getComments(postId, cb){ setTimeout(() => cb(null, [{ id: 99, by: 3, text: "Nice!" }]), 500); }
function getAuthor(userId, cb) { setTimeout(() => cb(null, { id: userId, name: "Abul Qasim" }), 500); }
function sendThanks(user, cb)  { setTimeout(() => cb(null, "thanked " + user.name), 500); }

// The task: user -> posts -> comments -> comment author -> send thanks
getUser(1, function (err, user) {
  if (err) { console.log("could not load user"); return; }
  console.log("1. got user:", user.name);
  getPosts(user.id, function (err, posts) {
    if (err) { console.log("could not load posts"); return; }
    console.log("2. got " + posts.length + " post(s)");
    getComments(posts[0].id, function (err, comments) {
      if (err) { console.log("could not load comments"); return; }
      console.log("3. got comment:", comments[0].text);
      getAuthor(comments[0].by, function (err, author) {
        if (err) { console.log("could not load author"); return; }
        console.log("4. comment author:", author.name);
        sendThanks(author, function (err, status) {
          if (err) { console.log("could not send thanks"); return; }
          console.log("5. done:", status);
          // Homework: try inserting ONE new step in the middle 😈
        });
      });
    });
  });
});
  • Reads diagonally — the logic drifts right instead of down.
  • 5 separate error checks — no single place to catch failures.
  • Fragile — inserting or reordering one step means re-nesting everything below it.
The fix, since ES6 (2015): Promises.

6 Promises

Key terms pending fulfilled rejected .then() .catch() .finally() chaining

Promise = an object representing a future value. An IOU: "you'll get the value — or a reason why not."

The three states

pending → settles exactly oncefulfilled (value) or rejected (reason).
⏳ pending
The promise is pending — its .then() and .catch() handlers are waiting…

Once settled, the buttons stop working — a promise settles only once.

Creating a promise

  • You get two functions from JS: call resolve(value) on success, reject(reason) on failure.
creating-a-promise.js
const myPromise = new Promise((resolve, reject) => {
  let success = true;              // flip to false → rejection path

  setTimeout(() => {
    if (success) {
      resolve("Promise resolved successfully!");
    } else {
      reject("Promise rejected! Something went wrong.");
    }
  }, 2000);
});

myPromise
  .then((message) => console.log("SUCCESS:", message))
  .catch((error) => console.log("FAILURE:", error));

Consuming with .then()

  • .then(callback) = "when fulfilled, run this with the value".
  • Registering is instant — the callback runs later.
  • Shortcuts: Promise.resolve(v) / Promise.reject(r) create already-settled promises.
consuming.js
const setTimeoutPromise = (time) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve();
    }, time);
  });
};

console.log("Before setTimeoutPromise");

setTimeoutPromise(1000).then(() => console.log("one second later"));

console.log("After setTimeoutPromise");

Chaining

.then() returns a new promise — whatever you return becomes the next .then()'s input.
chaining.js
const fetchData = new Promise((resolve) => {
  setTimeout(() => {
    resolve(10);
  }, 1000);
});

fetchData
  .then((number) => {
    console.log("First then:", number);    // 10
    return number * 2;
  })
  .then((newNumber) => {
    console.log("Second then:", newNumber); // 20
    return newNumber + 5;
  })
  .then((finalResult) => {
    console.log("Final result:", finalResult); // 25
  });

Escaping callback hell

Return promises instead of taking callbacks → the pyramid becomes a flat pipeline.
promise-chain.js
function fetchUser(userId) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("Fetched user");
      resolve({ id: userId, name: "Dinn" });
    }, 1000);
  });
}

function fetchPosts(user) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("Fetched posts for " + user.name);
      resolve(["Post 1", "Post 2"]);
    }, 1000);
  });
}

function fetchComments(posts) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("Fetched comments for " + posts[0]);
      resolve(["Comment 1", "Comment 2"]);
    }, 1000);
  });
}

// Flat. Readable. Top-to-bottom.
fetchUser(1)
  .then(fetchPosts)
  .then(fetchComments)
  .then((comments) => {
    console.log("Final data:", comments);
  });

Errors: .then / .catch / .finally

MethodRuns when…
.then(cb)fulfilled — and continues the chain.
.catch(cb)any earlier step rejects or throws — one catch guards the whole chain.
.finally(cb)always — cleanup (e.g. hide the spinner).
catch-finally.js · run it a few times
function getData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (Math.random() < 0.5) {       // coin toss
        resolve("fresh data");
      } else {
        reject(new Error("server timeout"));
      }
    }, 800);
  });
}

getData()
  .then((data) => console.log("Step 1 complete:", data))
  .then(() => console.log("Step 2 complete"))
  .catch((error) => console.log("Something went wrong:", error.message))
  .finally(() => console.log("All done! (runs either way)"));

Callbacks vs. Promises

FeatureCallbacksPromises
DefinitionFunction passed in, run laterObject holding a future value
SyntaxNested functions.then() / .catch()
ReadabilityCallback hellFlat chains
Error handlingManual, every levelOne .catch()
ChainingHard — nestingNatural

7 Async / await — the modern way

Key terms async → returns a Promise await → pauses the function try / catch ES2017
Syntax sugar over Promises — async code that looks synchronous. Nothing new underneath.

async functions return promises

Returning a value
a.js
async function f() {
  return 1;
}

f().then(console.log); // 1
Returning a promise — identical
b.js
async function f() {
  return Promise.resolve(1);
}

f().then(console.log); // 1

Rules of the road

  • await only inside async functions (or module top level).
  • Fulfilled → returns the value. Rejected → throws.
  • Pauses the function, not the browser — everything else keeps running.
  • Awaits run one after another — great for dependent steps.
breakfast.js
function makeCoffee() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("☕ Coffee is ready!");
    }, 2000);                       // 2-second brew
  });
}

async function prepareBreakfast() {
  console.log("Preparing pancakes...");
  const coffee = await makeCoffee();   // pause HERE until resolved
  console.log(coffee);
  console.log("🥞 Breakfast is ready!");
}

prepareBreakfast();
console.log("(meanwhile, the rest of the program keeps running…)");

Same logic, two styles

Promise chain
then-style.js
function loadData() {
  fetchUser(1)
    .then(function (user) {
      return fetchPosts(user.id);
    })
    .then(function (posts) {
      return fetchComments(posts[0].id);
    })
    .then(function (comments) {
      console.log(comments);
    })
    .catch(function (error) {
      console.log(error);
    });
}
Async / await — reads like a recipe
await-style.js
async function loadData() {
  try {
    let user     = await fetchUser(1);
    let posts    = await fetchPosts(user.id);
    let comments = await fetchComments(posts[0].id);
    console.log(comments);
  } catch (error) {
    console.log(error);
  }
}

Rejections → try / catch

try-catch.js
function riskyOperation() {
  return new Promise((resolve, reject) => {
    setTimeout(() => reject(new Error("the server is on fire 🔥")), 700);
  });
}

async function main() {
  try {
    console.log("Attempting operation…");
    const result = await riskyOperation();   // THROWS on rejection
    console.log("Success:", result);         // skipped
  } catch (error) {
    console.log("Caught it:", error.message);
  }
}

main();

The golden rule: every await that can reject must be covered by try/catch — otherwise: Uncaught (in promise).

8 The Fetch API

Key terms fetch(url) Response object response.ok response.json() two awaits
fetch(url) reads a URL and returns a Promise for a Response — the envelope, not the data.
MemberGives you
response.statusHTTP status code (200, 404, 500…)
response.oktrue for 200–299 — always check!
response.text()Body as a string — returns a Promise
response.json()Body parsed into a JS object — also a Promise

Two awaits, not one: reading the body is a second async stepawait fetch(url), then await response.json().

Fetch with .then() — live demo

  • Loads anime.json — a real file next to this page.
fetch-then.js · live network request
fetch("anime.json")
  .then((response) => response.json())   // parse body as JSON
  .then((data) => {
    console.log("Loaded", data.length, "anime");
    console.log("First title:", data[0].title);
  })
  .catch((error) => console.error("Error:", error.message));

The robust version — check response.ok

fetch only rejects on network failure — a 404 still "succeeds"! Check response.ok before parsing.
fetch-robust.js · live network request
const apiUrl = "https://jsonplaceholder.typicode.com/posts";

fetch(apiUrl)
  .then((response) => {
    if (!response.ok) {
      throw new Error("Network response was not ok: " + response.status);
    }
    return response.json();
  })
  .then((data) => {
    console.log("Received", data.length, "posts");
    console.log("First post title:", data[0].title);
  })
  .catch((error) => {
    console.error("Problem with the fetch operation:", error.message);
  });

Fetch with async/await — walkthrough

load-json.js · live network request
async function loadJson() {
  const response = await fetch("anime.json");
  const json = await response.json();
  console.log("Got " + json.length + " anime:", json.map(a => a.title).join(", "));
}

loadJson();
console.log("after loadJson");
  1. loadJson() starts.
  2. First await: fetch starts → function pauses → engine exits and prints "after loadJson".
  3. Response arrives → function resumes — the await gives us the response.
  4. Second await: parsing → pauses again.
  5. json is now a real array → final log runs.
"after loadJson" prints first — section 2's event loop, now with real network requests.

9 Choosing a technique

Callbacks (old)Promises (ES6 · 2015)Async/await (ES2017)
StyleNested functions.then().catch() chainLooks synchronous
ErrorsManual, each levelOne .catch()try/catch
Dependent stepsPyramid of doomFlat chainOne line per step
DebuggingHardMediumEasiest
Still used forEvent listeners, timersLibraries, Promise.allNew app code
Rule of thumb: new code → async/await + try/catch. Event listeners → always callbacks.

10 Key takeaways

  1. JS is single-threaded — the event loop + Web APIs fake multitasking.
  2. setTimeout never blocks — later code runs first; the callback waits for an empty stack.
  3. Callback = function passed in, called later. Nesting them = callback hell.
  4. Promise = future value. pending → fulfilled / rejected, settles once.
  5. .then / .catch / .finally — chain by returning values.
  6. async returns a Promise; await pauses the function (not the browser) and throws on rejection → try/catch.
  7. fetch → Response envelope → check response.okresponse.json() (second await).

11 Self-check quiz

No grades, instant feedback. Can you get all six?

Q1. What does this print, in order?
console.log("A"); setTimeout(() => console.log("B"), 0); console.log("C");

Even with a 0 ms delay, the callback goes through Web API → callback queue → event loop, which only delivers it once the call stack is empty. So: A, C, B.

Q2. Which are the three states of a Promise?

A promise starts pending, and settles exactly once into fulfilled (resolved) or rejected.

Q3. What does an async function always return?

async function f() { return 1; } returns a promise resolving to 1 — that's why f().then(...) works.

Q4. While an async function is paused at an await, the browser is…

Only that one function is suspended. The main thread is released — that's the whole point of async programming.

Q5. Why do we call response.json() after fetch()?

fetch resolves with a Response envelope. Reading + parsing the body is a second asynchronous operation — its own await or .then().

Q6. A promise rejects inside an async function with no try/catch. What happens?

The golden rule: every await that can reject needs try/catch (or a .catch on the returned promise).

12 Reading & practice

Zybooks — Chapter 8 "More JavaScript": sections 8.13 – 8.15. Do the participation activities — they mirror this week's lab.

Challenge: fetch user #1 from JSONPlaceholder, then their todos (/users/1/todos), and log how many are completed.