HTML, CSS, JavaScript Basics

Chapter 12
Asynchronous JavaScript and Browser APIs

Bachelors Program in Full Stack Development
HTML, CSS, JavaScript Basics   •   Bachelor

Asynchronous JavaScript and Browser APIs

Introduction and Context

Modern web applications rarely receive all their data when the initial HTML document loads. A weather dashboard retrieves forecasts from a remote service, an online store requests product information from a server, and a collaborative application receives updates while the user is working. These operations depend on asynchronous JavaScript: a programming model in which a task may begin now but complete later, without unnecessarily blocking the browser’s user interface.

Asynchronous programming is not simply a collection of syntax features. It is a way of reasoning about time, ordering, failure, cancellation, data validity, and user experience. JavaScript provides callbacks, promises, and the `async`/`await` syntax for representing asynchronous work. The browser supplements the language with APIs such as `fetch`, Web Storage, timers, and the Document Object Model (DOM).

This chapter explains how these mechanisms cooperate. It also considers responsible data handling: validating external input, protecting sensitive information, responding to network failure, avoiding unsafe DOM insertion, and designing interfaces that remain understandable while work is in progress.

Learning Objectives

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

1. Explain the JavaScript execution model, including the call stack, task queue, microtask queue, and event loop.

2. Distinguish synchronous and asynchronous operations.

3. Implement asynchronous workflows with callbacks, promises, and `async`/`await`.

4. Use `fetch` to make HTTP requests and handle response and network errors.

5. Store and retrieve suitable data with `localStorage` and `sessionStorage`.

6. Apply selected browser APIs to retrieve, persist, and present external data.

7. Design interfaces that address loading, success, empty, and failure states.

8. Evaluate security, privacy, accessibility, and reliability implications of browser-side data handling.

1. The Browser as an Event-Driven Environment

A browser executes JavaScript on a main thread in most ordinary web pages. The main thread performs JavaScript execution, DOM manipulation, style calculation, layout, and painting. If JavaScript performs a long calculation, the page may become unresponsive because the browser cannot process user input or render updates promptly.

JavaScript itself is commonly described as single-threaded: one call stack executes one piece of JavaScript at a time. However, browsers provide host facilities for timers, network operations, user input, and other activities. These facilities can complete independently and place callbacks into queues for later execution. Thus, JavaScript can coordinate many operations without running multiple JavaScript statements simultaneously on the same call stack.

The call stack

The call stack records currently executing functions. When a function is called, a stack frame is added; when it returns, that frame is removed. A function that calls another function creates a nested sequence of frames. An excessively deep or non-terminating sequence can cause a stack overflow.

Host APIs and queues

Browser APIs manage operations that JavaScript does not complete immediately. For example, `setTimeout` registers a timer, and `fetch` initiates a network request. When an operation is ready to notify JavaScript, its continuation is placed in a queue rather than interrupting currently executing code.

Two queue categories are especially important:

  • Task queue: commonly contains timer callbacks, user-interface events, and initial script execution tasks.
  • Microtask queue: commonly contains promise reactions, such as functions passed to `.then`, `.catch`, and `.finally`, and functions queued with `queueMicrotask`.

The event loop

The event loop coordinates execution approximately as follows:

1. If the call stack is empty, select an eligible task.

2. Execute that task to completion.

3. Drain the microtask queue, executing each microtask until the queue is empty.

4. The browser may update rendering between iterations, depending on timing and user-agent behavior.

5. Continue with another task.

The word “approximately” matters. HTML and browser specifications define detailed scheduling rules, and rendering is not guaranteed after every task. Nevertheless, the model accurately explains most application behavior.

Consider:

```js

console.log('A');

setTimeout(() => console.log('B'), 0);

Promise.resolve().then(() => console.log('C'));

console.log('D');

```

The output is normally:

```text

A

D

C

B

```

The synchronous statements execute first. The promise reaction becomes a microtask and is processed after the current task but before the timer task. A delay of zero in `setTimeout` does not mean “execute immediately”; it means “make this callback eligible after the minimum delay and after currently scheduled work permits it.”

A long chain of microtasks can delay timers, input, and rendering. Therefore, microtasks are useful for short follow-up work, not for an unbounded computation loop.

2. Callbacks: The Original Asynchronous Pattern

A callback is a function supplied to another function so that it can be invoked later. A simple timer illustrates the pattern:

```js

setTimeout(() => {

console.log('The timer completed');

}, 1000);

```

The callback is not executed when it is defined. It is invoked by the browser after the timer becomes eligible and the event loop reaches it.

Callbacks can represent success and failure explicitly:

```js

function loadProfile(onSuccess, onError) {

// A real implementation might make a network request.

const profile = { name: 'Amina' };

onSuccess(profile);

}

loadProfile(

profile => console.log(profile.name),

error => console.error(error)

);

```

Callbacks remain useful for event listeners and APIs designed around events:

```js

button.addEventListener('click', event => {

console.log('Clicked at', event.clientX, event.clientY);

});

```

However, callback-based workflows become difficult to read when operations depend on one another. Nested callbacks may produce “callback pyramids,” and error handling can become inconsistent. A callback may also be invoked too many times, not at all, or with arguments that are poorly documented. Promises were introduced to provide a more consistent representation of a future result.

3. Promises and Composable Asynchronous Work

A promise is an object representing the eventual outcome of an asynchronous operation. It has three conceptual states:

  • Pending: completion has not yet occurred.
  • Fulfilled: the operation completed successfully with a value.
  • Rejected: the operation failed with a reason.

A promise settles only once: it changes from pending to fulfilled or rejected, and cannot later change to the other state.

```js

const result = new Promise((resolve, reject) => {

const successful = true;

if (successful) {

resolve('Completed');

} else {

reject(new Error('Operation failed'));

}

});

```

Consumers attach reactions using `.then`, `.catch`, and `.finally`:

```js

result

.then(value => {

console.log(value);

})

.catch(error => {

console.error(error.message);

})

.finally(() => {

console.log('Finished');

});

```

A key property is that `.then` returns a new promise. This enables chaining:

```js

getUser()

.then(user => getOrders(user.id))

.then(orders => displayOrders(orders))

.catch(error => displayError(error));

```

If a handler returns a value, the next promise is fulfilled with that value. If it returns a promise, the next promise adopts that promise’s eventual state. If it throws, the next promise is rejected. This behavior makes errors propagate through a chain until a rejection handler handles them.

Promise concurrency methods

Several methods express different coordination policies:

  • `Promise.all(iterable)` fulfills when every input fulfills, but rejects as soon as one rejects. It is appropriate when all results are required.
  • `Promise.allSettled(iterable)` waits for every input and reports each outcome. It is useful when partial success is acceptable.
  • `Promise.race(iterable)` settles when the first input settles. It is often used for timeouts, although cancellation must be handled separately.
  • `Promise.any(iterable)` fulfills when the first input fulfills and rejects only when all inputs reject.

For example:

```js

const [profile, preferences] = await Promise.all([

fetchProfile(),

fetchPreferences()

]);

```

These operations run concurrently rather than waiting for the first request to finish before starting the second. Concurrency should nevertheless be used thoughtfully: many simultaneous requests can overload a server, consume bandwidth, or trigger rate limits.

4. Async Functions and `await`

An `async` function always returns a promise. Within such a function, `await` pauses that function’s continuation until the awaited promise settles; it does not block the browser’s entire JavaScript thread.

```js

async function loadMessage() {

const response = await fetch('/api/message');

const data = await response.json();

return data.text;

}

loadMessage()

.then(message => console.log(message))

.catch(error => console.error(error));

```

A rejected awaited promise causes an exception at the `await` expression. `try...catch...finally` can express the workflow clearly:

```js

async function showMessage() {

showLoading();

try {

const response = await fetch('/api/message');

if (!response.ok) {

throw new Error(`HTTP status: ${response.status}`);

}

const data = await response.json();

renderMessage(data);

} catch (error) {

showError('The message could not be loaded.');

console.error(error);

} finally {

hideLoading();

}

}

```

A frequent performance mistake is using sequential `await` statements for independent work:

```js

const a = await getA();

const b = await getB();

```

If the operations are independent, start them before awaiting:

```js

const aPromise = getA();

const bPromise = getB();

const [a, b] = await Promise.all([aPromise, bPromise]);

```

Sequential execution is correct when the second operation depends on the first. The design question is dependency, not merely syntax.

5. Fetch and HTTP Data Retrieval

The Fetch API provides a promise-based interface for HTTP requests. A basic request is:

```js

const response = await fetch('/api/products');

const products = await response.json();

```

`fetch` rejects its promise for some network-level failures, such as an unavailable network or a blocked request. It does not normally reject merely because the server responds with HTTP 404 or 500. The program must inspect `response.ok` or `response.status`.

```js

async function getProducts() {

const response = await fetch('/api/products', {

headers: { Accept: 'application/json' }

});

if (!response.ok) {

throw new Error(`Request failed with status ${response.status}`);

}

return response.json();

}

```

Request configuration

A request may specify a method, headers, and body:

```js

await fetch('/api/products/42', {

method: 'PUT',

headers: {

'Content-Type': 'application/json',

Accept: 'application/json'

},

body: JSON.stringify({ price: 19.99 })

});

```

The server must validate the body independently. Client-side validation improves usability but is not a security boundary.

Cancellation and timeouts

A request may become irrelevant when a user navigates away or enters a new search term. `AbortController` supports cancellation:

```js

const controller = new AbortController();

fetch('/api/search?q=javascript', { signal: controller.signal })

.then(response => response.json())

.catch(error => {

if (error.name !== 'AbortError') console.error(error);

});

// Later:

controller.abort();

```

An application can combine a controller with a timer to implement a request deadline. Cancellation prevents continued work, but it does not guarantee that a server has undone processing already performed.

Response validation

Valid JSON is not necessarily valid application data. The program should verify expected structure before rendering:

```js

function isProduct(value) {

return value &&

typeof value.id === 'number' &&

typeof value.name === 'string' &&

typeof value.price === 'number';

}

```

For production systems, schema validation libraries can provide more comprehensive checks. External data should be treated as untrusted input.

6. Presenting Data Safely and Accessibly

A complete data interface normally has at least four states: loading, success, and failure, with an empty-result variation of success. Users need feedback while a request is pending and an actionable message when it fails.

When displaying text received from an external source, prefer `textContent`:

```js

const heading = document.querySelector('#product-name');

heading.textContent = product.name;

```

Assigning untrusted content to `innerHTML` can create a cross-site scripting vulnerability if the content contains executable markup. If HTML must be generated dynamically, use safe DOM construction or a rigorously maintained sanitization solution. Never assume that data is safe merely because it came from an API.

Accessibility should also be considered. A loading indicator can be associated with a region using appropriate semantic markup and ARIA attributes, but ARIA should supplement, not replace, meaningful HTML. Error messages should be visible and understandable, and keyboard users must be able to operate controls.

Race conditions are another presentation concern. Suppose a user searches for “cat” and immediately searches for “car.” The first request might finish last and incorrectly replace the newer results. Solutions include aborting the previous request or assigning each request an identifier and ignoring stale responses.

7. Web Storage and Client-Side Persistence

The Web Storage API provides key-value storage associated with an origin:

  • `localStorage` persists across browser sessions until removed by the application or user.
  • `sessionStorage` generally persists for the lifetime of a page session and is separated by browsing context.

Both store strings. Objects must be serialized:

```js

const settings = { theme: 'dark', pageSize: 20 };

localStorage.setItem('settings', JSON.stringify(settings));

const raw = localStorage.getItem('settings');

const restored = raw ? JSON.parse(raw) : null;

```

`JSON.parse` can throw if stored content is malformed. Storage may also be unavailable or fail, for example because of browser privacy settings or quota restrictions. Production code should handle such failures.

Storage is synchronous. Large reads and writes can block the main thread, so it is unsuitable for substantial datasets. IndexedDB is a better browser API for larger structured data and more complex offline applications.

Do not store passwords, authentication tokens, financial information, or other sensitive secrets in Web Storage merely for convenience. Any script running in the origin can generally read it, including malicious code introduced through an XSS vulnerability. Secure, HTTP-only cookies can protect some session credentials from direct JavaScript access, but cookie design involves server configuration, CSRF defenses, expiration, and other security considerations.

A useful pattern is to cache non-sensitive data with an explicit version and expiration policy:

```js

const cacheRecord = {

version: 1,

savedAt: Date.now(),

data: products

};

localStorage.setItem('products-cache', JSON.stringify(cacheRecord));

```

The application should check the age and version before using the cache, and it should recover gracefully if the record is obsolete.

8. Selected Browser APIs in Application Design

Asynchronous applications often combine several APIs:

  • DOM API: selects elements, updates content, and registers events.
  • Fetch API: communicates with HTTP services.
  • Web Storage API: persists small, non-sensitive preferences or cache records.
  • URL and URLSearchParams: construct and inspect correctly encoded URLs.
  • AbortController: cancels fetches and other abortable operations.
  • Geolocation API: requests location with explicit user permission; location data is sensitive and should be minimized.
  • Notifications API: may display notifications only after permission and should not be used manipulatively.

Browser permissions are a governance mechanism, not an inconvenience to bypass. An application should explain why access is needed, request it at an appropriate moment, and provide useful functionality when permission is denied.

9. Detailed Illustrative Case: A Resilient Course Catalogue

Background

A university course catalogue displays courses retrieved from a remote service. Students can filter by subject and expect results to appear quickly, even when the network is unreliable. The application remembers the last selected subject but must not store student records or credentials in the browser.

Problem

A first implementation fetches data on every search, renders with `innerHTML`, gives no loading feedback, and allows older requests to overwrite newer results. It also assumes every response is successful JSON.

Analysis

The system has four related risks:

1. Network requests may fail or return an unsuccessful HTTP status.

2. Responses may arrive out of order.

3. External course titles may contain unsafe markup.

4. Repeated requests create unnecessary latency and server load.

The application needs explicit state transitions, cancellation, validation, safe rendering, and a limited cache.

Approach and Solution

The following illustrative implementation uses a request controller and safe text insertion:

```js

let activeController = null;

async function searchCourses(subject) {

activeController?.abort();

activeController = new AbortController();

setStatus('Loading courses…');

const url = `/api/courses?subject=${encodeURIComponent(subject)}`;

try {

const response = await fetch(url, {

signal: activeController.signal,

headers: { Accept: 'application/json' }

});

if (!response.ok) {

throw new Error(`HTTP ${response.status}`);

}

const data = await response.json();

if (!Array.isArray(data)) {

throw new Error('Unexpected response format');

}

renderCourses(data);

localStorage.setItem('last-subject', subject);

setStatus(data.length ? `${data.length} courses found.` : 'No courses found.');

} catch (error) {

if (error.name === 'AbortError') return;

setStatus('Courses could not be loaded. Please try again.');

console.error(error);

}

}

function renderCourses(courses) {

const list = document.querySelector('#course-list');

list.replaceChildren();

for (const course of courses) {

const item = document.createElement('li');

item.textContent = `${course.code}: ${course.title}`;

list.append(item);

}

}

```

The service should additionally validate query parameters, enforce authorization where appropriate, apply rate limits, and return a documented schema. The client should not treat cached data as authoritative when the catalogue has changed. A timestamped cache could support an offline or stale-data mode while clearly labelling the result.

Outcome

The revised design avoids stale-result races by aborting the previous request, distinguishes network and HTTP failures, avoids unsafe HTML interpretation, provides feedback, and stores only a harmless preference. It also establishes a foundation for testing success, empty, malformed, slow, and failed responses.

Learning Points

The case demonstrates that asynchronous programming is a systems-design concern. Correct syntax alone does not ensure correct behavior. The interface must define what happens when operations overlap, data is incomplete, permissions are denied, or the network is unavailable.

10. Challenges, Limitations, and Professional Practice

Error handling is layered

An application should consider transport failure, HTTP failure, parsing failure, schema failure, authorization failure, and rendering failure. A generic user message may be appropriate externally, while detailed diagnostics are recorded securely for developers. Sensitive data should not be written to logs.

CORS and origin boundaries

A browser enforces the same-origin policy. Cross-origin requests may require the server to provide suitable Cross-Origin Resource Sharing headers. Client code cannot grant itself permission by adding arbitrary request headers. CORS is not an authentication mechanism; it controls which browser origins may read responses.

Performance

Avoid unnecessary sequential waits, repeated requests, excessive DOM updates, and large synchronous storage operations. Debouncing search input can reduce request frequency, but it should not be used to conceal slow or unreliable services. Rendering large results may require pagination, virtualization, or incremental updates.

Privacy and regulation

External data may include personal information. Data minimization, purpose limitation, retention limits, transparency, and user choice are important principles in privacy governance. Legal obligations differ by jurisdiction, so developers should consult institutional policies and applicable law rather than assuming that technical feasibility implies permission.

Testing asynchronous code

Tests should cover deterministic outcomes rather than relying on arbitrary delays. Mock network responses, test rejected promises, simulate malformed JSON, verify that loading indicators are removed in `finally`, and test that stale responses are ignored. Browser automation can verify keyboard access and visible error states.

Contemporary developments

Modern applications increasingly use service workers, IndexedDB, streaming responses, and framework data layers. Service workers can support caching and offline behavior, but introduce lifecycle and cache-invalidation complexity. Streams can process large responses incrementally, while Web Workers can move CPU-intensive calculations away from the main thread. These tools extend the same fundamental principles: explicit state, controlled concurrency, cancellation where possible, validation, and responsible data use.

Best-Practice Checklist

  • Keep synchronous main-thread work short.
  • Treat every external response as untrusted and validate its shape.
  • Check `response.ok`; do not assume `fetch` rejects on HTTP errors.
  • Use `try...catch...finally` for understandable asynchronous control flow.
  • Run independent operations concurrently with `Promise.all` when appropriate.
  • Cancel obsolete requests and prevent stale responses from updating the interface.
  • Show loading, success, empty, and failure states.
  • Use `textContent` or safe DOM APIs for untrusted text.
  • Store only small, non-sensitive data in Web Storage.
  • Plan for unavailable storage, denied permissions, and offline conditions.
  • Protect credentials on the server and follow institutional privacy requirements.
  • Test timing, failure, accessibility, and security—not only the successful path.

Chapter Summary

Asynchronous JavaScript allows a browser application to coordinate timers, user events, and network operations without blocking the main thread unnecessarily. The event loop executes call-stack work, processes queued tasks, and drains promise microtasks according to browser scheduling rules. Callbacks provide the original continuation model; promises provide composable states and error propagation; and `async`/`await` offers readable syntax over promises.

The Fetch API retrieves HTTP data but requires explicit checking of HTTP status and response structure. `AbortController` supports cancellation, which is important when requests become obsolete. Web Storage provides simple synchronous persistence for small, non-sensitive strings, while larger or more complex data may require IndexedDB. Safe DOM updates, accessible status messages, validation, caching policies, and privacy-aware permission handling are essential parts of professional browser development.

The central design lesson is that asynchronous code must model uncertainty: completion occurs later, operations may overlap, data may be invalid, and services may fail. Reliable applications make these conditions visible, manageable, and safe.

Key Terms

Event loop: The browser mechanism that coordinates the call stack, queued work, microtasks, and rendering opportunities.

Call stack: The execution structure containing currently active JavaScript function calls.

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

Microtask: High-priority queued continuation work, commonly created by promise reactions.

Callback: A function supplied for execution at a later time or in response to an event.

Promise: An object representing the eventual fulfillment or rejection of an asynchronous operation.

Pending: The unsettled state of a promise.

Fulfilled: The successful settled state of a promise.

Rejected: The failed settled state of a promise.

Promise chain: A sequence of promise reactions in which each step produces a new promise.

Async function: A function that always returns a promise and permits `await` expressions.

Fetch API: A browser API for making promise-based network requests.

AbortController: An API for signalling cancellation to abortable operations.

Web Storage: The browser key-value storage facility containing `localStorage` and `sessionStorage`.

Same-origin policy: A browser security boundary restricting interactions between different origins.

CORS: HTTP-based rules that allow controlled cross-origin browser requests.

Schema validation: Checking that received data has the expected structure and types.

Race condition: Incorrect behavior caused by the relative timing or ordering of concurrent operations.

Cross-site scripting: Injection of executable content into a page through unsafe handling of input.

Data minimization: The privacy principle of collecting and retaining only data necessary for a stated purpose.

Review Questions

Short Answer Questions

1. What is the difference between a task and a microtask in the browser event loop?

2. Why does `fetch` require an explicit check of `response.ok`?

3. What are the three conceptual states of a promise?

4. Why is `textContent` generally safer than assigning untrusted data to `innerHTML`?

5. What is the principal difference between `localStorage` and `sessionStorage`?

Long Answer Questions

1. Explain how callbacks, promises, and `async`/`await` represent the same general problem while differing in composability and readability.

2. Describe the lifecycle of a Fetch request, including network failure, unsuccessful HTTP status, parsing failure, and schema validation.

3. Explain why independent asynchronous operations may be started concurrently and discuss situations in which sequential execution remains necessary.

4. Discuss the security, privacy, and performance limitations of Web Storage.

5. Design the loading, success, empty, and failure states for a browser application that displays external data.

Analytical / Application Questions

1. A search interface sends a request for “design,” followed immediately by a request for “development.” The first response arrives last. Explain the defect and propose two ways to prevent stale results from appearing.

2. A developer writes `await fetch('/api/items')` and assumes that a 500 response will enter `catch`. Analyze this assumption and provide a corrected approach.

3. An application stores a user’s access token in `localStorage` so that users remain signed in. Evaluate the design from security and usability perspectives and recommend a safer architecture.

Multiple Choice Questions with Answers

Question 1

Which sequence best describes the usual relationship between a completed task and promise reactions?

A. The task runs, then eligible promise microtasks are processed before the next task.

B. Promise microtasks always run before the task that created them.

C. The next timer task always runs before promise reactions.

D. Rendering must occur between every task and every microtask.

Correct Answer: A. The task runs, then eligible promise microtasks are processed before the next task.

Question 2

What does an async function always return?

A. A string

B. A promise

C. A callback

D. A DOM element

Correct Answer: B. A promise

Question 3

Which statement about fetch is correct?

A. It rejects automatically for every HTTP 4xx and 5xx response.

B. It returns parsed JSON automatically.

C. It may fulfill with a response whose HTTP status is 404 or 500.

D. It can only make GET requests.

Correct Answer: C. It may fulfill with a response whose HTTP status is 404 or 500.

Question 4

Which method fulfills only after all supplied promises fulfill, and rejects if one rejects?

A. Promise.race

B. Promise.any

C. Promise.allSettled

D. Promise.all

Correct Answer: D. Promise.all

Question 5

Which API is designed to signal cancellation to a fetch request?

A. AbortController

B. StorageEvent

C. URLSearchParams

D. MutationObserver

Correct Answer: A. AbortController

Question 6

Why should an application validate the structure of parsed JSON?

A. JSON parsing guarantees application-specific types.

B. Valid JSON may still have an unexpected shape or data types.

C. Validation is required only for local constants.

D. Validation prevents all network failures.

Correct Answer: B. Valid JSON may still have an unexpected shape or data types.

Question 7

Which statement about Web Storage is correct?

A. It stores JavaScript objects without serialization.

B. It is asynchronous and suitable for very large databases.

C. It stores string values and can block because its operations are synchronous.

D. It is automatically encrypted by the browser.

Correct Answer: C. It stores string values and can block because its operations are synchronous.

Question 8

Which approach is generally safest for inserting an external course title as plain text?

A. element.textContent = title

B. element.innerHTML = title

C. document.write(title)

D. eval(title)

Correct Answer: A. element.textContent = title

Question 9

What is a suitable reason to use Promise.all for two requests?

A. The second request must receive the first request’s result.

B. Both requests are independent and both results are required.

C. The requests must be performed one after another.

D. One request should be cancelled when the other succeeds.

Correct Answer: B. Both requests are independent and both results are required.

Question 10

What is a likely consequence of an unbounded microtask chain?

A. It can delay timers, input handling, and rendering.

B. It permanently disables the network.

C. It converts all promises into callbacks.

D. It guarantees faster screen updates.

Correct Answer: A. It can delay timers, input handling, and rendering.

HTML, CSS, JavaScript Basics
Chapter 12 of 15