HTML, CSS & JavaScript

Chapter 12
Asynchronous JavaScript, APIs and Browser Storage

Certificate in Full stack programming
HTML, CSS & JavaScript   •   Certificate

Asynchronous JavaScript, APIs and Browser Storage

Introduction

Modern web applications must respond to users while waiting for work that may take an unpredictable amount of time. A page may request data from a server, read a file, wait for a timer, or communicate with a device. If JavaScript stopped the browser during each operation, the interface would become unresponsive. Asynchronous JavaScript provides a way to begin a task and continue executing other code while the task completes.

This chapter introduces the browser event loop, callbacks, promises, async/await, the Fetch API, JSON, error handling, and browser storage. These features are central to applications such as dashboards, shopping carts, messaging systems, registration forms, and offline-capable websites. They also create responsibilities: developers must protect sensitive information, handle unreliable networks, respect privacy, and design predictable user experiences.

Learning Objectives

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

  • describe the event loop at an introductory level;
  • distinguish synchronous and asynchronous operations;
  • use callbacks, promises, and async/await;
  • retrieve and process JSON using fetch();
  • handle network, HTTP, parsing, and application failures;
  • compare cookies, localStorage, and sessionStorage;
  • select storage mechanisms with privacy and security awareness;
  • combine asynchronous requests and browser storage in a practical application.

1. Synchronous and Asynchronous Execution

In synchronous execution, statements generally run from top to bottom. Each statement must finish before the next begins.

console.log("First");
console.log("Second");
console.log("Third");

The output order is predictable: First, Second, Third. A long computation or blocking operation can prevent later code from running and can make a browser interface appear frozen.

Asynchronous execution allows JavaScript to start an operation and receive its result later. The browser performs many operations outside the JavaScript call stack, including timers, network requests, and some file operations. JavaScript can therefore continue handling events such as clicks and keyboard input.

Asynchronous does not mean that JavaScript executes two statements simultaneously on the same call stack. Browser JavaScript is commonly described as single-threaded at the language execution level. Asynchronous behavior is coordinated by the browser environment and the event loop. Some browser or server environments may use additional threads internally, but this does not change the programming model introduced here.

2. The Event Loop

The event loop coordinates three important ideas:

  1. Call stack: the functions currently being executed.
  2. Web APIs or host services: browser-provided facilities such as timers and networking.
  3. Task queues: queues containing callbacks ready to run.

When a function is called, it is placed on the call stack. When it returns, it is removed. If an asynchronous operation is started, the browser manages that operation. When it completes, its continuation is placed in an appropriate queue. The event loop checks whether the call stack is empty and, when it is, moves eligible queued work to the stack.

Promises use a microtask queue. Microtasks are normally processed after the current synchronous task finishes and before the browser proceeds to the next ordinary task, such as a timer callback. This ordering explains why promise handlers can run before a timer scheduled for the same general period.

console.log("A");

setTimeout(() => console.log("B: timer"), 0);

Promise.resolve().then(() => console.log("C: promise"));

console.log("D");

A typical output is:

A
D
C: promise
B: timer

The timer does not run immediately. A delay of zero means “make the callback eligible after the current work,” not “interrupt the current code.” The precise scheduling environment can contain additional details, but the important rule is that synchronous code completes before queued asynchronous callbacks execute.

A useful mental model is:

JavaScript code → call stack → asynchronous browser service
                         ↑              ↓
                    event loop ← task or microtask queue

Long-running synchronous code can still block the interface. Asynchronous APIs help with waiting, but they do not automatically make CPU-intensive calculations non-blocking.

3. Callbacks

A callback is a function supplied to another function so that it can be called later.

function loadMessage(callback) {
  setTimeout(() => {
    callback(null, "Data loaded");
  }, 500);
}

loadMessage((error, message) => {
  if (error) {
    console.error(error);
    return;
  }
  console.log(message);
});

This example uses a common error-first convention: the first argument represents an error and the second represents successful data. The convention is not enforced by JavaScript; it is a design choice used by many APIs and libraries.

Callbacks are appropriate for simple events and one-step operations. However, deeply nested callbacks can become difficult to read and maintain. This problem, sometimes called “callback hell,” is reduced by promises and async/await.

Callbacks also require careful attention to repeated execution. An event listener may run many times, whereas a network completion callback normally represents one completion. A developer should document whether a callback is called zero, one, or many times.

4. Promises

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

StateMeaning
PendingThe operation has not completed.
FulfilledThe operation completed successfully with a value.
RejectedThe operation failed with a reason or error.

A settled promise cannot change from fulfilled to rejected or vice versa. The main methods are:

  • .then() handles fulfillment;
  • .catch() handles rejection;
  • .finally() runs after settlement, whether successful or unsuccessful.
function getGreeting() {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve("Hello"), 300);
  });
}

getGreeting()
  .then(message => {
    console.log(message);
  })
  .catch(error => {
    console.error("Could not obtain greeting", error);
  })
  .finally(() => {
    console.log("Request finished");
  });

A .then() handler can return a value, producing a fulfilled promise, or return another promise, producing a chain. If a handler throws an exception, the resulting promise is rejected and can be handled by a later .catch().

getUser()
  .then(user => getOrders(user.id))
  .then(orders => displayOrders(orders))
  .catch(error => showError(error));

Promises make the relationship between sequential asynchronous steps explicit. Independent tasks can be coordinated with Promise.all():

const [profile, notifications] = await Promise.all([
  fetchProfile(),
  fetchNotifications()
]);

Promise.all() fulfills when every input promise fulfills. It rejects as soon as one rejects. If all results are needed regardless of individual failures, Promise.allSettled() is often more suitable.

5. async and await

An async function always returns a promise. The await keyword pauses that function until a promise settles, while allowing the browser to continue other work. It does not freeze the entire browser.

async function showGreeting() {
  try {
    const greeting = await getGreeting();
    console.log(greeting);
  } catch (error) {
    console.error(error);
  }
}

await can be used inside an async function and, in supported environments, at the top level of JavaScript modules. The style often resembles synchronous code while retaining asynchronous behavior.

Sequential code is appropriate when the second operation depends on the first:

const user = await fetchUser();
const orders = await fetchOrders(user.id);

If operations are independent, starting them together can reduce waiting time:

const userPromise = fetchUser();
const settingsPromise = fetchSettings();

const [user, settings] = await Promise.all([
  userPromise,
  settingsPromise
]);

The second pattern should be used only when parallel requests are acceptable for the server, user, and application logic. Excessive parallel requests can waste bandwidth or trigger rate limits.

6. APIs, HTTP and JSON

An application programming interface (API) is a defined way for software components to communicate. A web API commonly uses HTTP requests and responses. A request may include a method, URL, headers, and an optional body. A response includes a status code, headers, and content.

Common HTTP methods include:

MethodTypical purpose
GETRetrieve a resource.
POSTSubmit data or create a resource.
PUT/PATCHReplace or partially update a resource.
DELETERequest removal of a resource.

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

Example JSON:

{
  "id": 17,
  "name": "Amina",
  "active": true,
  "roles": ["student", "editor"]
}

The JavaScript object and JSON text are related but not identical. JSON.stringify(value) converts a JavaScript value to JSON text; JSON.parse(text) converts valid JSON text to a JavaScript value. Parsing untrusted or malformed text can throw an exception.

7. Fetch with JSON

The Fetch API returns a promise for a Response object.

async function loadProducts() {
  const response = await fetch("/api/products");

  if (!response.ok) {
    throw new Error(`HTTP error: ${response.status}`);
  }

  const products = await response.json();
  return products;
}

A critical detail is that fetch() normally rejects for network-level failures, such as a failed connection or a blocked request. It does not automatically reject for HTTP statuses such as 404 or 500. The program should inspect response.ok or response.status.

A complete request with error handling might be written as follows:

async function loadProducts() {
  try {
    const response = await fetch("/api/products", {
      method: "GET",
      headers: {
        "Accept": "application/json"
      }
    });

    if (!response.ok) {
      throw new Error(`Server returned ${response.status}`);
    }

    const data = await response.json();

    if (!Array.isArray(data)) {
      throw new Error("Unexpected response format");
    }

    return data;
  } catch (error) {
    console.error("Product loading failed:", error);
    throw error;
  }
}

For a JSON POST request:

async function createProduct(product) {
  const response = await fetch("/api/products", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Accept": "application/json"
    },
    body: JSON.stringify(product)
  });

  if (!response.ok) {
    throw new Error(`Creation failed: ${response.status}`);
  }

  return response.json();
}

The server must validate the submitted data. Client-side validation improves usability but cannot be treated as a security boundary.

Failure categories

A robust application distinguishes several failures:

  • Network failure: the request could not be completed.
  • HTTP failure: the server responded with an error status.
  • Parsing failure: the response was not valid JSON of the expected type.
  • Application failure: the response is technically valid but represents an unsuccessful operation.
  • Cancellation or timeout: the user no longer needs the result or the operation takes too long.

Requests can be cancelled with AbortController:

async function loadWithCancellation(signal) {
  const response = await fetch("/api/report", { signal });
  if (!response.ok) throw new Error("Report request failed");
  return response.json();
}

const controller = new AbortController();
loadWithCancellation(controller.signal).catch(error => {
  if (error.name !== "AbortError") console.error(error);
});

// Later, for example when leaving the page:
controller.abort();

User-facing error messages should be useful without exposing secrets, stack traces, database details, or internal server structure. Logging should also avoid passwords, access tokens, and unnecessary personal data.

8. Browser Storage

Browser storage allows a website to retain information between interactions. The correct mechanism depends on duration, transmission requirements, sensitivity, and scope.

Cookies

Cookies are small name-value data items associated with a domain and path. They are automatically included in matching HTTP requests, subject to cookie rules. Servers commonly use cookies for sessions, preferences, and security controls.

Important cookie attributes include:

  • Secure: send the cookie only over HTTPS;
  • HttpOnly: prevent JavaScript from reading the cookie;
  • SameSite: restrict cross-site sending and help reduce cross-site request forgery risk;
  • Max-Age or Expires: control lifetime;
  • Path and possibly Domain: control scope.

A server can set a cookie with an HTTP response header. JavaScript can access non-HttpOnly cookies through document.cookie, but the format is a single string and requires parsing. Cookies have limited capacity and are sent with requests, creating bandwidth and privacy considerations.

localStorage

localStorage stores string values associated with an origin and normally persists after the browser is closed. It is useful for non-sensitive preferences such as a theme choice.

localStorage.setItem("theme", "dark");
const theme = localStorage.getItem("theme") || "light";
localStorage.removeItem("theme");

Objects must be serialized:

const settings = { language: "en", compact: true };
localStorage.setItem("settings", JSON.stringify(settings));

const saved = localStorage.getItem("settings");
const parsedSettings = saved ? JSON.parse(saved) : null;

localStorage is synchronous. Very large operations can delay the main thread. Storage may also be unavailable, restricted, cleared, or limited by browser privacy settings. Never assume that it is an indestructible database.

sessionStorage

sessionStorage has a similar interface but is scoped to a browser tab or window session. It is useful for temporary form state, navigation information, or a multi-step process that should not normally remain after the tab is closed.

FeatureCookieslocalStoragesessionStorage
Automatically sent to serverYes, when applicableNoNo
Typical lifetimeConfigurablePersistentTab session
JavaScript accessUnless HttpOnlyYesYes
StoresText, small dataStringsStrings
Best examplesSessions, server preferencesNon-sensitive preferencesTemporary tab state

The Web Storage APIs are synchronous and provide no built-in encryption. Any JavaScript running in the origin can usually read them. Therefore, an access token in localStorage may be exposed if the site suffers a cross-site scripting vulnerability. Sensitive authentication designs often prefer secure, HttpOnly, SameSite cookies managed by the server, although the correct choice depends on the architecture and threat model.

9. Privacy, Security and Governance

Storage decisions should follow data minimization: collect and retain only what the application needs. Developers should identify whether data is personal, sensitive, confidential, or merely functional. They should communicate purposes clearly and follow applicable laws, institutional policies, and consent requirements.

Important practices include:

  • do not store passwords in browser storage;
  • avoid storing sensitive personal data unless necessary;
  • use HTTPS for authenticated applications;
  • configure cookies with suitable Secure, HttpOnly, and SameSite attributes;
  • validate and authorize requests on the server;
  • treat data from APIs and storage as untrusted input;
  • escape or safely render user-provided content to reduce XSS risk;
  • provide a method to clear or update stored data;
  • consider shared computers, browser profiles, backups, and extensions;
  • explain retention and tracking behavior in an appropriate privacy notice.

Cross-origin requests are controlled by browser security policies. Cross-Origin Resource Sharing (CORS) allows a server to state which origins may access selected responses. CORS is not a replacement for authentication or authorization. A server must still verify identity and permissions.

10. Applied Case: A Course Dashboard

Background

Consider an illustrative course dashboard that displays a learner’s profile, enrolled modules, and recent announcements. It should load data from an API, remember the preferred display theme, and show useful messages when the network is unavailable.

Problem

The first implementation makes three requests one after another, stores the user profile in localStorage, and assumes every response is successful. It is slow, may display stale personal data, and provides no meaningful failure handling.

Analysis

The profile, modules, and announcements are independent requests, so they can be initiated together. The theme is a low-risk preference and is suitable for localStorage. The profile should not be cached indefinitely without considering privacy and freshness. The interface should distinguish a temporary network failure from an unauthorized response.

Approach and Solution

const themeKey = "course-dashboard-theme";

function applyTheme() {
  const theme = localStorage.getItem(themeKey) || "light";
  document.documentElement.dataset.theme = theme;
}

async function getJson(url) {
  const response = await fetch(url, {
    headers: { "Accept": "application/json" }
  });

  if (!response.ok) {
    const error = new Error(`Request failed with ${response.status}`);
    error.status = response.status;
    throw error;
  }

  return response.json();
}

async function loadDashboard() {
  applyTheme();

  const statusElement = document.querySelector("#status");
  statusElement.textContent = "Loading dashboard…";

  try {
    const [profile, modules, announcements] = await Promise.all([
      getJson("/api/profile"),
      getJson("/api/modules"),
      getJson("/api/announcements")
    ]);

    renderProfile(profile);
    renderModules(modules);
    renderAnnouncements(announcements);
    statusElement.textContent = "Dashboard loaded.";
  } catch (error) {
    if (error.status === 401 || error.status === 403) {
      statusElement.textContent = "Please sign in to view this dashboard.";
    } else {
      statusElement.textContent =
        "The dashboard could not be loaded. Check your connection and try again.";
    }
    console.error("Dashboard error", error);
  }
}

The code uses parallel requests, checks HTTP status explicitly, avoids placing profile data in persistent storage, and gives the user a safe general message. In a production system, the server would control authentication and authorization, and the interface would also address loading states, retries, cancellation, and partial results.

Outcome and Learning Points

This design reduces unnecessary waiting and separates concerns: getJson() handles transport and parsing, while rendering functions handle presentation. The case demonstrates that asynchronous programming is not merely a syntax choice. It affects responsiveness, failure behavior, privacy, and the overall reliability of the application.

11. Best Practices and Limitations

Use one consistent asynchronous style within a function or module. Prefer async/await for multi-step workflows, while understanding promises because the two are closely related. Always handle rejected promises. Check HTTP status codes. Validate response shapes before using them. Avoid unnecessary sequential waits and avoid uncontrolled request bursts.

Storage is not a substitute for a server database. Browser data can be deleted, modified, unavailable, or isolated by privacy features. Quotas vary by browser and context. Web Storage operations can block the main thread. Cookies are transmitted automatically and therefore should remain small and carefully scoped.

Retries require judgment. Retrying a GET may be reasonable after a temporary failure, but blindly retrying a non-idempotent POST can create duplicate records. A retry strategy should use limits, delays, cancellation, and server-supported idempotency where appropriate.

Contemporary applications may also use IndexedDB for larger structured client-side data, service workers for caching and offline behavior, and streaming APIs for incremental responses. These are extensions of the same underlying concerns: lifecycle, failure, consistency, performance, and privacy.

Chapter Summary

Asynchronous JavaScript enables web applications to remain responsive while waiting for timers, network operations, and other browser services. The event loop coordinates the call stack, host APIs, and queues. Callbacks provide a basic completion mechanism; promises represent eventual success or failure; and async/await offers readable promise-based control flow.

The Fetch API retrieves resources asynchronously. Developers must check both network errors and HTTP status codes, parse JSON carefully, validate response structures, and provide safe user-facing error messages. Promise.all() is useful for independent requests, while sequential await is appropriate when one operation depends on another.

Cookies, localStorage, and sessionStorage differ in lifetime, scope, transmission behavior, and security properties. Sensitive information should not be placed casually in JavaScript-readable storage. Secure application design combines HTTPS, suitable cookie attributes, server-side validation and authorization, data minimization, and privacy-aware retention policies.

Key Terms

Event loop: The mechanism that coordinates JavaScript execution with queued asynchronous work.

Call stack: The structure containing functions currently executing.

Callback: A function supplied to be called later.

Promise: An object representing the eventual result of an asynchronous operation.

Pending: The initial unsettled state of a promise.

Fulfilled: The successful settled state of a promise.

Rejected: The failed settled state of a promise.

Microtask: Queued work, including promise handlers, normally processed before the next ordinary task.

Fetch API: A browser API for making asynchronous network requests.

Response: The object representing an HTTP response returned by fetch().

JSON: A text-based format for structured data exchange.

HTTP status: A server response code indicating the result category of a request.

Cookie: Small data associated with a domain and potentially sent with HTTP requests.

localStorage: Persistent, origin-scoped string storage in the browser.

sessionStorage: Tab-session-scoped string storage in the browser.

CORS: A browser-controlled mechanism for permitting selected cross-origin requests.

HttpOnly: A cookie attribute that prevents ordinary JavaScript access.

SameSite: A cookie attribute controlling cross-site sending behavior.

Data minimization: The practice of collecting and retaining only necessary data.

AbortController: An API used to signal cancellation of operations such as fetch requests.

Review Questions

5 Short Answer Questions

  1. What is the purpose of the event loop?
  2. What are the three conceptual states of a promise?
  3. Why should code check response.ok after calling fetch()?
  4. Give one suitable use for sessionStorage.
  5. What security benefit does the HttpOnly cookie attribute provide?

5 Long Answer Questions

  1. Explain how the call stack, browser APIs, task queues, and event loop cooperate during an asynchronous request.
  2. Compare callbacks, promises, and async/await, including one advantage and limitation of each.
  3. Describe a robust strategy for handling network, HTTP, parsing, and application failures.
  4. Compare cookies, localStorage, and `sessionStorage in terms of lifetime, transmission, access, and appropriate use.
  5. Explain why storing authentication tokens in JavaScript-readable browser storage can create security risks.

3 Analytical / Application Questions

  1. A dashboard makes three independent API requests one after another. Analyze the performance and reliability implications, then propose a revised design.
  2. Design a storage policy for a shopping website that needs a theme preference, a temporary checkout step, and an authenticated session. Justify each storage choice.
  3. A POST request appears to fail, so the client automatically retries it five times. Analyze the possible consequences and propose a safer retry and cancellation strategy.

Multiple Choice Questions with Answers

Question 1

Which statement best describes the event loop?

A. It permanently stores data in the browser.

B. It moves eligible queued work to the call stack when JavaScript is ready.

C. It converts JSON into HTML automatically.

D. It guarantees that all network requests finish in order.

Correct Answer: B. It moves eligible queued work to the call stack when JavaScript is ready.

Question 2

What does an async function return?

A. Always a string

B. A timer identifier

C. A promise

D. A JSON object only

Correct Answer: C. A promise

Question 3

What normally happens when fetch() receives an HTTP 404 response?

A. The promise always rejects automatically.

B. The browser changes the response to status 200.

C. The promise usually fulfills with a Response whose ok property is false.

D. The response is automatically retried.

Correct Answer: C. The promise usually fulfills with a Response whose ok property is false.

Question 4

Which method converts a JavaScript object into JSON text?

A. JSON.parse()

B. JSON.stringify()

C. Response.json()

D. Object.toJSONText()

Correct Answer: B. JSON.stringify()

Question 5

Which Promise method waits for all supplied promises to fulfill and rejects if one rejects?

A. Promise.race()

B. Promise.any()

C. Promise.all()

D. Promise.resolve()

Correct Answer: C. Promise.all()

Question 6

Which storage mechanism is normally scoped to a browser tab session?

A. sessionStorage

B. localStorage

C. A permanent cookie only

D. The call stack

Correct Answer: A. sessionStorage

Question 7

Which cookie attribute prevents ordinary JavaScript from reading the cookie?

A. Secure

B. Path

C. HttpOnly

D. Max-Age

Correct Answer: C. HttpOnly

Question 8

Why is localStorage generally unsuitable for storing passwords?

A. It cannot store strings.

B. It is readable by JavaScript running in the origin and is not inherently encrypted.

C. It is automatically sent with every HTTP request.

D. It exists only while a tab remains open.

Correct Answer: B. It is readable by JavaScript running in the origin and is not inherently encrypted.

Question 9

What is a suitable reason to use Promise.all()?

A. To run independent asynchronous operations concurrently and obtain all results.

B. To ensure every request is sent sequentially.

C. To suppress all rejected promises permanently.

D. To convert a response into a cookie.

Correct Answer: A. To run independent asynchronous operations concurrently and obtain all results.

Question 10

What does AbortController allow a program to do with a fetch request?

A. Change a 404 response into a 200 response.

B. Cancel the request by using its signal.

C. Encrypt JSON automatically.

D. Store the response in sessionStorage automatically.

Correct Answer: B. Cancel the request by using its signal.

HTML, CSS & JavaScript
Chapter 12 of 15