Introduction to Web Technologies (HTML, CSS, JS)

Chapter 12
Asynchronous JavaScript and Web APIs

Diploma in Full Stack Programming
Introduction to Web Technologies (HTML, CSS, JS)   •   Diploma

Asynchronous JavaScript and Web APIs

Introduction

Modern web applications communicate continuously with browsers, servers, files, sensors, and third-party services. These operations often take an unpredictable amount of time. If JavaScript waited synchronously for every network response, the browser interface would become unresponsive. Asynchronous JavaScript allows a program to begin a time-consuming operation and continue doing other work until the result is available.

This chapter explains the browser event loop, callbacks, promises, and async/await. It then applies these ideas to the Fetch API, JSON, browser storage, URL APIs, and external services. Particular attention is given to loading states, errors, cancellation, security, accessibility, and maintainable professional code.

Learning Objectives

By the end of this chapter, a learner should be able to:

  • describe the call stack, task queues, microtask queue, and event loop;
  • distinguish synchronous, callback-based, promise-based, and async/await code;
  • request and process data with Fetch and JSON;
  • manage loading, success, empty, and failure states;
  • select appropriate browser storage mechanisms;
  • construct and inspect URLs safely using the URL API;
  • integrate an external service while considering security and reliability;
  • cancel requests and avoid common asynchronous programming errors.

1. Why Asynchronous Programming Is Necessary

JavaScript in a browser normally executes on a single main thread. This thread performs JavaScript work, updates the Document Object Model (DOM), calculates styles, and helps render the interface. A long-running operation on that thread can prevent clicks, scrolling, animation, and visual updates.

Some operations are relatively slow because they depend on something outside the JavaScript engine:

  • an HTTP request must travel through a network;
  • a file must be read from storage;
  • a timer must reach its scheduled time;
  • a user must provide input.

The browser provides Web APIs for these activities. JavaScript starts the operation, registers what should happen later, and returns control to the browser. When the operation completes, the associated callback is placed in a queue for later execution.

Asynchronous does not mean parallel JavaScript execution. It means that completion is separated in time from initiation. The browser may perform network work independently, but JavaScript callbacks still execute according to the event-loop rules.

2. The Event Loop Model

The event loop coordinates JavaScript execution and asynchronous events. A simplified model contains the following parts:

  1. Call stack: the currently executing JavaScript functions.
  2. Web APIs: browser-managed facilities such as timers, Fetch, and DOM events.
  3. Task queue: callbacks from events, timers, and some other operations.
  4. Microtask queue: promise reactions and queueMicrotask callbacks.
  5. Event loop: a repeating process that moves eligible work to the call stack.

JavaScript executes one stack frame at a time. When the stack becomes empty, the runtime processes microtasks before taking the next task. Consequently, a promise reaction usually runs before a timer callback that is already waiting in the task queue.

Simplified browser event loop Call stackJavaScript now Web APIsFetch, timers, DOM Task queueevents and timers Microtask queuepromise reactions The event loop selects microtasks before the next task when the stack is empty.
Simplified browser event loop showing call stack, Web APIs, microtasks, tasks, and rendering
Figure: Simplified browser event loop showing call stack, Web APIs, microtasks, tasks, and rendering

Figure: A simplified event-loop relationship among JavaScript, browser APIs, and queues.

A small ordering example

console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
console.log("D");

The output is A, D, C, B. The synchronous statements run first. The promise reaction is a microtask and is processed before the timer task. A zero-millisecond timer does not mean immediate execution; it means that the callback becomes eligible after the current work and scheduling rules permit it.

A long chain of microtasks can delay timers and rendering. Therefore, asynchronous code can still harm responsiveness if it performs excessive work without yielding.

3. Callbacks, Promises, and async/await

3.1 Callbacks

A callback is a function supplied to be executed later.

button.addEventListener("click", () => {
  console.log("The button was activated");
});

Callbacks are suitable for independent events, but deeply nested callbacks can produce difficult-to-read “callback pyramids.” Error handling and control flow become harder when several dependent operations are nested.

3.2 Promises

A Promise represents the eventual result of an asynchronous operation. It has three conceptual states:

StateMeaning
PendingThe operation has not settled.
FulfilledThe operation completed successfully.
RejectedThe operation failed.

A promise settles only once. then() handles fulfillment, catch() handles rejection, and finally() runs after either outcome.

loadProfile()
  .then(profile => renderProfile(profile))
  .catch(error => showError(error.message))
  .finally(() => hideSpinner());

Promise.all() is appropriate when several independent operations are all required. It fulfills with an array of results, but rejects as soon as one input promise rejects. Promise.allSettled() waits for every operation and reports each outcome, which is useful for dashboards where partial success is acceptable. Promise.race() settles with the first settled promise, while Promise.any() fulfills with the first fulfilled promise and rejects only when all inputs reject.

3.3 async and await

An async function always returns a promise. The await expression pauses that function until a promise settles; it does not block the browser’s main thread.

async function showProfile() {
  try {
    const response = await fetch("/api/profile");
    if (!response.ok) throw new Error(`HTTP error: ${response.status}`);
    const profile = await response.json();
    renderProfile(profile);
  } catch (error) {
    showError("The profile could not be loaded.");
    console.error(error);
  }
}

A common mistake is forgetting that fetch() rejects for network-level failures but normally fulfills for HTTP statuses such as 404 or 500. The response.ok property must therefore be checked explicitly.

Independent requests should not be unnecessarily serialized:

const [products, categories] = await Promise.all([
  fetch("/api/products").then(checkResponse).then(r => r.json()),
  fetch("/api/categories").then(checkResponse).then(r => r.json())
]);

4. Fetch and HTTP Communication

Fetch is the modern browser API for HTTP requests. Its basic sequence is:

  1. create a request;
  2. await the response headers;
  3. check the response status;
  4. read the body, often with json();
  5. update the interface.
async function checkResponse(response) {
  if (!response.ok) {
    throw new Error(`Request failed with status ${response.status}`);
  }
  return response;
}

async function getCourses() {
  const response = await fetch("/api/courses");
  await checkResponse(response);
  return response.json();
}

For a JSON POST request, the client usually sets the content type and serializes the JavaScript value:

const response = await fetch("/api/courses", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ title: "Web Technologies", level: "Diploma" })
});
await checkResponse(response);
const created = await response.json();

The server must validate all received data. Client-side validation improves usability but is not a security boundary.

Cancellation and time limits

An AbortController can cancel a request when a user navigates away or starts a newer search.

const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 8000);

try {
  const response = await fetch("/api/search?q=html", {
    signal: controller.signal
  });
  await checkResponse(response);
} catch (error) {
  if (error.name === "AbortError") {
    console.log("Request cancelled");
  } else {
    console.error(error);
  }
} finally {
  clearTimeout(timeout);
}

Cancellation prevents obsolete work from updating the interface. It does not guarantee that a remote server has undone work already performed.

5. JSON Data Interchange

JSON (JavaScript Object Notation) is a text format for structured data. It supports objects, arrays, strings, numbers, Boolean values, and null. It does not support JavaScript functions, undefined, or class instances as native JSON types.

const text = '{"name":"Ada","active":true}';
const person = JSON.parse(text);
const output = JSON.stringify(person);

JSON.parse() can throw a SyntaxError when the input is malformed, so external data should be parsed inside appropriate error handling. JSON also has no built-in schema guarantee. A professional client should verify that required fields have expected types before rendering them.

Never insert untrusted strings into the page with innerHTML without sanitization. Prefer textContent for plain text. This reduces the risk of cross-site scripting (XSS).

6. Loading, Empty, Success, and Failure States

A reliable interface models asynchronous work as visible states rather than merely showing a spinner. A useful state model is:

  • idle: no request has begun;
  • loading: a request is in progress;
  • success: valid data is available;
  • empty: the request succeeded but returned no useful records;
  • error: the operation failed;
  • cancelled: the operation was intentionally stopped.
async function loadCourses() {
  setState("loading");
  try {
    const response = await fetch("/api/courses");
    await checkResponse(response);
    const courses = await response.json();

    if (!Array.isArray(courses) || courses.length === 0) {
      setState("empty");
      return;
    }
    renderCourses(courses);
    setState("success");
  } catch (error) {
    setState(error.name === "AbortError" ? "cancelled" : "error");
  }
}

Loading indicators should be perceivable and should not cause layout instability. Error messages should be understandable to users, while detailed diagnostic information belongs in logs. Retry actions should be deliberate; automatic retries need limits and backoff to avoid overwhelming a service.

For search-as-you-type interfaces, debounce input so that a request begins only after a short period without typing. Also cancel or ignore earlier requests. Otherwise, a slower earlier response may overwrite newer results.

7. Browser Storage

Browser storage allows data to persist between page visits, but each mechanism has a different purpose.

MechanismLifetime and characteristicsAppropriate use
CookiesSent with matching HTTP requests; small; controlled by security attributesSessions and server-related state
sessionStoragePer-origin and per-tab; usually ends when the tab closesTemporary form or navigation state
localStoragePer-origin; persists across browser sessions; string values onlySmall non-sensitive preferences
IndexedDBAsynchronous database with structured recordsLarger offline data and application caches
Cache APIStores request/response objects, often with service workersOffline and performance caching
localStorage.setItem("theme", "dark");
const theme = localStorage.getItem("theme") ?? "light";
localStorage.removeItem("theme");

Storage operations can throw, for example when storage is blocked or unavailable. Values must be serialized:

localStorage.setItem("settings", JSON.stringify({ compact: true }));
const settings = JSON.parse(localStorage.getItem("settings") || "null");

Do not store passwords, access tokens, or other sensitive secrets in localStorage; any script running in the origin can read them. Cookies used for sessions should generally use Secure, HttpOnly, and an appropriate SameSite policy. Storage is subject to origin boundaries and quota limits, and it is not a substitute for authoritative server-side data.

8. URL and URLSearchParams APIs

Constructing URLs by string concatenation is error-prone because escaping, existing query parameters, and fragments may be mishandled. The URL API provides structured manipulation.

const url = new URL("https://example.com/search");
url.searchParams.set("q", "web APIs");
url.searchParams.set("page", "2");
console.log(url.href);

URLSearchParams automatically encodes parameter values. It can also read parameters from the current page:

const params = new URLSearchParams(window.location.search);
const query = params.get("q") || "";

Use history.pushState() or replaceState() to update application URLs without a full page reload, while ensuring that the resulting URL remains meaningful and accessible. Never trust URL parameters as safe HTML or as authorization instructions; validate them and encode output according to context.

9. External Services and Cross-Origin Rules

An external service may provide maps, payments, weather, authentication, analytics, or educational data. Integration requires more than writing a request. Developers must examine authentication, rate limits, service availability, data ownership, privacy terms, and failure behavior.

Browsers enforce the same-origin policy. A page may not freely read responses from another origin unless that origin permits access through Cross-Origin Resource Sharing (CORS). CORS is enforced by browsers; it is not an authentication mechanism. A public API key placed in frontend code should be regarded as visible. Secret credentials belong on a trusted server or backend proxy.

External data should be treated as untrusted input. Validate response structure, limit displayed content, use HTTPS, and avoid exposing personal information. If a service processes user data, obtain appropriate consent and follow applicable privacy and data-protection requirements.

10. Applied Case: A Course Search Interface

Background

A fictional college wants a course search page. Users enter a phrase, the page requests matching courses from /api/courses, displays results, and preserves the selected page size between visits.

Problem

The first prototype sends one request for every keystroke, displays no loading state, assumes all responses are successful, and allows an earlier slow response to replace newer results. It also stores a session token in localStorage.

Analysis

The interface has concurrency, reliability, and security problems. Requests need debouncing and cancellation. HTTP status and JSON shape must be checked. The token must be removed from client storage and managed through a secure session design.

Approach and solution

let activeController;
let searchTimer;

function scheduleSearch(term) {
  clearTimeout(searchTimer);
  searchTimer = setTimeout(() => searchCourses(term), 300);
}

async function searchCourses(term) {
  activeController?.abort();
  activeController = new AbortController();
  setState("loading");

  try {
    const url = new URL("/api/courses", window.location.origin);
    url.searchParams.set("q", term.trim());

    const response = await fetch(url, { signal: activeController.signal });
    await checkResponse(response);
    const data = await response.json();

    if (!Array.isArray(data.courses)) {
      throw new Error("Unexpected response format");
    }
    renderCourses(data.courses);
    setState(data.courses.length ? "success" : "empty");
  } catch (error) {
    if (error.name !== "AbortError") {
      setState("error");
    }
  }
}

The page may store a non-sensitive preference such as page size in localStorage, but authentication should use a safer server-managed design. Accessible status text should announce loading and errors, and the results region should be updated using safe text operations.

Outcome

The revised design reduces unnecessary requests, prevents stale responses from winning, distinguishes network failures from empty results, and provides a more predictable user experience. The example also demonstrates that asynchronous correctness includes interface state, data validation, and security—not only promise syntax.

Learning points

  • Control concurrency rather than assuming responses arrive in order.
  • Treat every external response as potentially malformed.
  • Separate user-facing messages from developer diagnostics.
  • Store only data appropriate for the selected storage mechanism.

11. Common Errors and Best Practices

Ignoring rejected promises: Use try/catch, .catch(), or deliberate error propagation.

Assuming Fetch rejects on 404: Check response.ok or the status code.

Reading a response body twice: A response body is generally consumed once; clone it only when there is a justified need.

Serializing independent requests: Use Promise.all() where operations do not depend on one another.

Updating removed elements: Check component lifetime or cancel work when a view is destroyed.

Unbounded retries: Use a retry limit, delay, and preferably exponential backoff with jitter.

Leaking sensitive information: Avoid secrets in source code, URLs, logs, and web storage.

Blocking the main thread: Break up heavy computation or move suitable work to a Web Worker. Asynchronous network code alone does not make CPU-heavy code non-blocking.

Poor accessibility: Announce state changes, preserve keyboard operation, and do not communicate status by color alone.

12. Contemporary Developments

Modern applications increasingly combine Fetch with service workers, the Cache API, WebSockets, Server-Sent Events, Web Workers, and framework data-loading systems. These technologies solve different problems: Fetch handles request-response operations; WebSockets support ongoing two-way communication; Server-Sent Events support server-to-client streams; workers move computation away from the main thread.

The underlying principles remain stable: make state explicit, validate boundaries, handle cancellation, protect credentials, and design for partial failure. Newer APIs such as AbortSignal.timeout() may simplify time limits where supported, but compatibility should be checked before production use.

Chapter Summary

Asynchronous JavaScript allows browsers to remain responsive while external operations complete. The event loop coordinates the call stack, browser APIs, task queue, and microtask queue; promise reactions are generally processed before the next task. Callbacks are foundational, promises compose asynchronous results, and async/await provides readable promise-based control flow.

Fetch requires explicit HTTP-status checking, structured JSON parsing, cancellation where appropriate, and careful management of loading, empty, success, and error states. localStorage, sessionStorage, cookies, IndexedDB, and the Cache API have different lifetimes and security properties. URL APIs safely construct and inspect query parameters. External services require attention to CORS, authentication, rate limits, privacy, validation, and failure.

Professional asynchronous code is not merely code that “waits.” It controls concurrency, communicates state to users, protects data, validates boundaries, and remains usable when networks and services fail.

Key Terms

Event loop: Runtime mechanism that schedules asynchronous callbacks for JavaScript execution.

Call stack: Structure containing currently executing JavaScript functions.

Task: A queued unit of work such as a timer or user-event callback.

Microtask: High-priority queued work such as a promise reaction.

Callback: Function executed in response to a later event or result.

Promise: Object representing a future fulfillment or rejection.

async function: Function that always returns a promise.

await: Expression that pauses an async function until a promise settles.

Fetch API: Browser interface for making network requests.

HTTP status: Numeric result indicating the outcome of an HTTP request.

JSON: Text-based format for exchanging structured data.

AbortController: Object used to cancel abortable asynchronous operations.

CORS: Browser-controlled mechanism allowing selected cross-origin requests.

Origin: Combination of scheme, host, and port used for web security boundaries.

localStorage: Persistent, origin-scoped string key-value storage.

sessionStorage: Temporary, origin- and tab-scoped string storage.

IndexedDB: Asynchronous browser database for structured records.

URLSearchParams: API for reading and constructing URL query parameters.

Debouncing: Delaying action until input has remained inactive for a specified period.

XSS: Injection of executable untrusted content into a web page.

Review Questions

Short Answer Questions

  1. What is the difference between a task queue and a microtask queue?
  2. Why must Fetch users check response.ok?
  3. State two appropriate uses of browser storage and one inappropriate use.
  4. What problem does AbortController solve?
  5. Why is JSON parsing of external data not sufficient validation?

Long Answer Questions

  1. Explain the event loop using the call stack, Web APIs, tasks, and microtasks.
  2. Compare callbacks, promises, and async/await, including strengths and limitations.
  3. Describe a robust Fetch workflow from request initiation to user-facing error handling.
  4. Compare cookies, localStorage, sessionStorage, IndexedDB, and the Cache API.
  5. Explain the security and governance considerations of integrating an external API.

Analytical / Application Questions

  1. A search page displays results from an older request after a user has typed a newer query. Diagnose the cause and design a correction.
  2. Design the state model and error-handling strategy for a dashboard that loads three independent data sets, where partial success is acceptable.
  3. Review a page that stores an access token in localStorage and inserts API-provided descriptions with innerHTML. Identify the risks and propose safer alternatives.

Multiple Choice Questions with Answers

Question 1

Which work is generally processed before the next task when the JavaScript call stack is empty?

A. A promise reaction in the microtask queue

B. A later network request that has not completed

C. A CSS file download

D. A newly created DOM element

Correct Answer: A. A promise reaction in the microtask queue

Question 2

What must a developer normally do after calling fetch() to detect an HTTP 404 or 500 response?

A. Check response.ok or response.status

B. Wait for setTimeout()

C. Call JSON.stringify()

D. Read localStorage

Correct Answer: A. Check response.ok or response.status

Question 3

Which Promise method waits for all input promises and reports both fulfilled and rejected outcomes?

A. Promise.race()

B. Promise.any()

C. Promise.allSettled()

D. Promise.resolve()

Correct Answer: C. Promise.allSettled()

Question 4

What does an async function return?

A. A DOM element

B. A promise

C. A timer identifier only

D. A JSON string

Correct Answer: B. A promise

Question 5

Which browser mechanism is generally most appropriate for a small non-sensitive preference that should persist across sessions?

A. localStorage

B. The call stack

C. A microtask

D. AbortController

Correct Answer: A. localStorage

Question 6

What is the main purpose of URLSearchParams?

A. To execute JavaScript in a URL

B. To construct and inspect query parameters

C. To store passwords securely

D. To replace the event loop

Correct Answer: B. To construct and inspect query parameters

Question 7

Why should a search interface cancel or ignore earlier requests?

A. To ensure an older, slower response cannot overwrite newer results

B. To make JSON valid

C. To bypass CORS

D. To make HTTP status codes unnecessary

Correct Answer: A. To ensure an older, slower response cannot overwrite newer results

Question 8

Which statement about CORS is correct?

A. It is a browser-enforced mechanism for permitted cross-origin reading

B. It encrypts passwords stored in localStorage

C. It guarantees that an API key is secret

D. It prevents all network failures

Correct Answer: A. It is a browser-enforced mechanism for permitted cross-origin reading

Question 9

Which operation converts a JavaScript object into JSON text?

A. JSON.parse()

B. JSON.stringify()

C. response.ok

D. URLSearchParams.get()

Correct Answer: B. JSON.stringify()

Question 10

Which practice reduces the risk of cross-site scripting when displaying untrusted plain text?

A. Insert it with innerHTML without checking

B. Place it in a script element

C. Use textContent for plain-text output

D. Save it as a cookie

Correct Answer: C. Use textContent for plain-text output

Introduction to Web Technologies (HTML, CSS, JS)
Chapter 12 of 15