EduFlow
JavaScript Fundamentals
0%

Section 4 · JavaScript in the Browser

Async JavaScript: Promises, async/await and fetch

22 min4 min readCode playground

Lesson at a glance

Explain why JavaScript waits without freezing, read and write promise-based code with then and async/await, call fetch with proper error handling, and predict output order using the event loop.

  1. 1Callbacks: “call me when it is done”
  2. 2Promises: a value that arrives later
  3. 3async and await: promises that read like normal code
  4. 4fetch, for real
  5. 5The event loop

No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.

Pocket would be more useful if it could show your spending in euros too, which means asking a server for today’s exchange rates. That request might take 50 milliseconds or five seconds. If JavaScript simply stopped and waited, the whole page would freeze: no scrolling, no typing, no clicks. Since your code shares one thread with the page itself, it needs a way to start slow work, carry on, and pick up the result later. That is what asynchronous code is.

Callbacks: “call me when it is done”

The oldest form of “later” is a callback. setTimeout asks the browser to call a function after a delay:

JavaScript
console.log("Requesting rates…");
setTimeout(() => {
  console.log("Rates arrived");
}, 100);
console.log("Still responsive");

“Still responsive” prints before “Rates arrived”. setTimeout does not pause anything; it hands the function to the browser and returns immediately. Callbacks work, but when step two needs step one’s result and step three needs step two’s, they nest deeper and deeper, and error handling has to be repeated at every level. Promises were added to fix that.

Promises: a value that arrives later

A promise is an object that stands for a result that is not ready yet. It starts pending, and settles exactly once: fulfilled with a value, or rejected with an error. You attach what should happen next with .then (on success) and .catch (on failure).

This course’s sandbox has no network, so the examples use fakeFetch, a stand-in that behaves like the real fetch: it returns a promise of a response object with ok, status and a json() method.

JavaScript
function fakeFetch(url) {
  return new Promise((resolve) => {
    setTimeout(() => {
      if (url === "/api/rates") {
        resolve({ ok: true, status: 200, json: async () => ({ EUR: 0.86, GBP: 0.75 }) });
      } else {
        resolve({ ok: false, status: 404, json: async () => ({ error: "Not found" }) });
      }
    }, 50);
  });
}

fakeFetch("/api/rates")
  .then((response) => response.json())
  .then((rates) => console.log("EUR rate:", rates.EUR))
  .catch((error) => console.log("Failed:", error.message));

Each .then returns a new promise, so steps chain in a flat line instead of nesting, and one .catch at the end handles a failure from any step. You will rarely create promises with new Promise yourself; libraries and browser APIs hand them to you, and your job is to consume them.

async and await: promises that read like normal code

async/await is a more readable way to write the same thing. Inside a function marked async, await promise pauses that function until the promise settles, then gives you its value. Errors become ordinary exceptions you catch with try/catch:

JavaScript
function fakeFetch(url) {
  return new Promise((resolve) => {
    setTimeout(() => {
      const ok = url === "/api/rates";
      const body = ok ? { EUR: 0.86 } : { error: "Not found" };
      resolve({ ok, status: ok ? 200 : 404, json: async () => body });
    }, 50);
  });
}

async function getJson(url) {
  const response = await fakeFetch(url);
  if (!response.ok) {
    throw new Error(`HTTP ${response.status} for ${url}`);
  }
  return response.json();
}

async function showRates() {
  try {
    const rates = await getJson("/api/rates");
    console.log("EUR:", rates.EUR);
    await getJson("/api/missing");
  } catch (error) {
    console.log("Could not load:", error.message);
  }
}

showRates();

Notice what await does not do: it does not freeze the page. While showRates waits, the browser keeps handling clicks and drawing frames. Only this one function is paused.

An async function always returns a promise, even when you return a plain value. So whoever calls it must await it too, or use .then.

fetch, for real

In a real page, you replace fakeFetch with the browser’s fetch, and the shape of the code stays the same:

rates.js
async function getJson(url) {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`HTTP ${response.status} for ${url}`);
  }
  return response.json();
}

The response.ok check is essential, and many tutorials leave it out. fetch only rejects when the request could not happen at all, for example when the user is offline. A 404 or a 500 is a perfectly good HTTP response, so the promise resolves, with ok set to false. Without the check, your code would try to read rates out of an error page.

When two requests do not depend on each other, start both and wait once with Promise.all (inside an async function, like every await), which fulfils with an array of results or rejects as soon as either fails:

rates.js
const [rates, categories] = await Promise.all([
  getJson("/api/rates"),
  getJson("/api/categories"),
]);

The event loop

How can one thread do all this? The engine runs your code on a call stack, one function at a time. Slow things, such as timers and network requests, are handled by the browser outside the stack. When one finishes, its callback is put in a queue. Whenever the stack is empty, the event loop takes the next callback from a queue and runs it.

Your code runs on the call stack. Timers and fetch are handled by the browser, which puts finished callbacks into queues. Promise callbacks go to the microtask queue, timers and events to the task queue. When the stack is empty, the event loop first runs all microtasks, then one task. Call stack showRates() main script Browser timers, fetch Microtasks promise callbacks Tasks timers, clicks 1. all microtasks 2. then one task
The event loop runs queued callbacks when the stack is empty

There are two queues, and the order between them explains many puzzles. Promise callbacks go into the microtask queue; timers and events go into the task queue. Each time the stack empties, the loop runs every waiting microtask first, then one task, then checks microtasks again:

JavaScript
console.log("1: script");
setTimeout(() => console.log("4: timeout task"), 0);
Promise.resolve().then(() => console.log("3: promise microtask"));
console.log("2: script");

A timeout of 0 ms does not mean “now”; it means “as a task, once the stack is clear and the microtasks are done”. The same model explains why a long loop freezes the page: while your code occupies the stack, no click handler, timer or redraw can run.

Section 4 is complete: Pocket lives on a page, reacts to clicks, takes input, saves data and can talk to a server. Section 5 turns this growing pile of code into a well-organised program.

Key takeaways

  • JavaScript runs one thing at a time; slow work such as network requests and timers happens outside your code, which is called back when it finishes.
  • A promise is an object standing for a value that will arrive later; it ends up fulfilled with a value or rejected with an error.
  • await pauses only the surrounding async function until a promise settles; wrap awaits in try/catch to handle rejections.
  • fetch rejects only when the network fails; for HTTP errors such as 404 it resolves with response.ok set to false, so always check it.
  • After each piece of synchronous code, the event loop runs all queued promise callbacks before the next timer or event.

Further reading

Check your understanding

4 questions

Question 1 of 4

In what order are the lines logged?

JavaScript
console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
console.log("D");
Question 2 of 4

const data = getRates(); console.log(data.EUR); where getRates is an async function. What is logged?

Question 3 of 4

Your server returns 404 Not Found for fetch("/api/rates"). What does the fetch promise do?

Question 4 of 4

You need the rates and the category list, from two independent URLs, before rendering. Which is fastest?

Practice

Code playground

Load data asynchronously

The sandbox has no network, so the starter gives you fakeFetch(url), which behaves like fetch: it returns a promise of a response with ok, status and an async json() method. Write four async functions:

  • getJson(url) returns the parsed body, and throws an Error whose message includes the status (such as 404) when response.ok is false.
  • loadCategoryNames() returns ["food", "travel", "fun"], the name of each item from /api/categories.
  • toEuroCents(cents) returns the amount converted with the EUR rate from /api/rates, rounded to whole cents.
  • safeGetJson(url, fallback) returns the JSON from url, or fallback if loading fails.
// A stand-in for fetch: the sandbox has no network. It returns a promise
// of a response with ok, status and json(), after a short delay.
function fakeFetch(url) {
  const routes = {
    "/api/rates": { EUR: 0.86, GBP: 0.75 },
    "/api/categories": [{ name: "food" }, { name: "travel" }, { name: "fun" }],
  };
  return new Promise((resolve) => {
    setTimeout(() => {
      const ok = url in routes;
      const body = ok ? routes[url] : { error: "Not found" };
      resolve({ ok, status: ok ? 200 : 404, json: async () => body });
    }, 30);
  });
}

// Return the parsed JSON body, or throw an Error mentioning the status
async function getJson(url) {
  const response = fakeFetch(url);
  return response.json();
}

// ["food", "travel", "fun"], from /api/categories
async function loadCategoryNames() {
  const categories = getJson("/api/categories");
  return categories.map((c) => c.name);
}

// The amount converted to euro cents (rounded), using /api/rates
async function toEuroCents(cents) {
}

// The JSON from url, or `fallback` if anything goes wrong
async function safeGetJson(url, fallback) {
  return getJson(url);
}
Loading the editor…

Ctrl/⌘ + Enter to run · Runs in a sandbox in your browser. Nothing leaves your device.

Console

(no output)

Checks

  • getJson returns the parsed body
  • getJson throws an Error mentioning 404 for a missing URL
  • loadCategoryNames returns the category names
  • toEuroCents converts with the EUR rate and rounds
  • safeGetJson returns the fallback when loading fails