Asynchronous Web Applications and APIsIntroductionModern web applications rarely perform all work in a single, uninterrupted sequence. A browser may request data from several services, wait for a database operation, upload a file, or receive events from a server while the user continues interacting with the page. Asynchronous programming allows an application to begin an operation and continue useful work before that operation finishes. This chapter examines asynchronous JavaScript, Promises, async/await, the Fetch API, JSON, RESTful API design, status codes, pagination, caching, versioning, GraphQL, real-time communication, and resilient error handling. These topics are central to building responsive, scalable, and maintainable web applications. Learning ObjectivesAfter completing this chapter, a learner should be able to: - explain the event loop and asynchronous execution;
- create and compose Promises using
async/await; - use Fetch to consume HTTP APIs and process JSON safely;
- interpret REST constraints, API contracts, and HTTP status codes;
- design pagination, caching, and versioning strategies;
- distinguish REST from GraphQL;
- select suitable real-time communication techniques;
- implement retries, timeouts, cancellation, validation, and meaningful error handling;
- evaluate security, reliability, and governance concerns in API development.
1. Synchronous and Asynchronous ExecutionIn synchronous execution, statements normally run in order. A function does not return control until its current operation has completed. This model is easy to understand, but it is unsuitable when an operation involves network or file input/output, because the processor may remain idle while the program waits. In browser JavaScript, long-running synchronous code blocks the main thread. Since the main thread also handles rendering and user input, excessive blocking produces slow scrolling, delayed clicks, and an unresponsive interface. Asynchronous operations are coordinated by the event loop. JavaScript executes ordinary code on a call stack. Browser or runtime APIs perform operations such as timers and network requests outside that stack. When an operation completes, its callback is placed into a queue. The event loop moves queued work to the call stack when the stack is empty. Promise reactions are generally scheduled through the microtask queue, which is processed before the next task such as a timer callback. Asynchronous execution does not necessarily mean that JavaScript executes two statements simultaneously on the same thread. It means that waiting for an external operation does not block the continuation of the program. console.log("A");
setTimeout(() => console.log("C"), 0);
console.log("B");The usual output is A, B, C, because the timer callback runs only after the current synchronous work has finished. 2. Promises and async/awaitA Promise represents the eventual result of an asynchronous operation. It has three conceptual states: - pending: the operation has not completed;
- fulfilled: the operation completed successfully;
- rejected: the operation failed.
A Promise settles only once. Its result can be consumed with .then() and failures can be handled with .catch(): fetch("/api/products")
.then(response => {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
})
.then(products => renderProducts(products))
.catch(error => showError(error));async/await is syntax built on Promises. An async function always returns a Promise. The await expression pauses that function until the awaited Promise settles; it does not block the browser's entire JavaScript environment.
async function loadProducts() {
try {
const response = await fetch("/api/products");
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`);
}
const products = await response.json();
renderProducts(products);
} catch (error) {
showError("Products could not be loaded.");
console.error(error);
}
}Sequential and concurrent operationsIf a second request depends on the result of a first request, sequential await is appropriate. If independent requests can proceed together, Promise.all() reduces total waiting time. const [categories, products] = await Promise.all([
fetch("/api/categories").then(checkResponse).then(r => r.json()),
fetch("/api/products").then(checkResponse).then(r => r.json())
]);Promise.all() rejects when any input Promise rejects. Promise.allSettled() waits for every operation and reports each outcome, making it useful when partial success is acceptable. Promise.race() settles when the first Promise settles, while Promise.any() fulfills when the first input fulfills and rejects only if all inputs reject.
A common mistake is to write array.forEach(async item => ...) and expect the outer function to wait. forEach does not consume the returned Promises. Use for...of for sequential processing or Promise.all(array.map(...)) for parallel processing. 3. Fetch, HTTP, and JSONThe Fetch API provides a Promise-based interface for HTTP requests. A response Promise may fulfill even when the server returns a client or server error. Therefore, applications must inspect response.ok or response.status. async function checkResponse(response) {
if (!response.ok) {
let detail = "Request failed";
try {
const body = await response.json();
detail = body.message || detail;
} catch (_) {
// The response may not contain JSON.
}
throw new Error(`${detail} (HTTP ${response.status})`);
}
return response;
}A JSON request commonly specifies both an HTTP method and a media type: const response = await fetch("/api/products", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "Keyboard", price: 29.99 })
});JSON represents objects, arrays, strings, numbers, booleans, and null. It does not preserve JavaScript-specific types such as Date, Map, Set, or functions. Dates are normally transmitted as strings and must be validated and converted explicitly. JSON numbers also require care where exact decimal arithmetic is important, such as currency; monetary values may instead be represented as integer minor units, for example cents. A robust client validates the response shape rather than assuming that syntactically valid JSON is semantically correct. A response containing {} is valid JSON but may violate the API contract if the application requires an array of products. Cancellation and timeoutsA request that is no longer relevant should be cancelled. AbortController supports cancellation of Fetch operations: const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch("/api/report", { signal: controller.signal });
clearTimeout(timer);
await checkResponse(response);
} catch (error) {
if (error.name === "AbortError") showError("The request timed out.");
else showError("The report could not be loaded.");
}Cancellation is especially valuable for search-as-you-type interfaces, where an earlier request may become irrelevant after a newer query is entered. 4. RESTful APIs and API ContractsREST, or Representational State Transfer, is an architectural style rather than a particular library. Its principal constraints include: - Client–server separation: the user interface and data service have distinct responsibilities.
- Statelessness: each request contains the information needed to process it; the server does not depend on hidden conversational state between requests.
- Cacheability: responses identify whether they may be cached.
- Uniform interface: resources, representations, standard methods, and self-descriptive messages are used consistently.
- Layered system: clients need not know whether they communicate directly with an origin server, gateway, or cache.
- Code-on-demand, an optional constraint: a server may transfer executable code to a client.
A resource-oriented API might use: | Operation | Method and URI | Typical meaning |
|---|
| List products | GET /api/products | Retrieve a collection | | Read product | GET /api/products/42 | Retrieve one resource | | Create product | POST /api/products | Create a subordinate resource | | Replace product | PUT /api/products/42 | Replace the representation | | Modify product | PATCH /api/products/42 | Partially update a resource | | Delete product | DELETE /api/products/42 | Remove a resource |
GET, PUT, and DELETE are generally intended to be idempotent: repeating the same request has the same intended effect as performing it once. POST is not inherently idempotent. If a network failure occurs after a payment request reaches the server but before the client receives the response, blindly retrying may create a duplicate transaction. An idempotency key can allow the server to recognize and safely repeat a request.
An API contract specifies endpoints, parameters, request and response schemas, authentication requirements, status codes, error formats, limits, and versioning policy. OpenAPI documents are often used to publish such contracts. A strong contract reduces ambiguity between frontend, backend, testing, and operations teams. 5. HTTP Status Codes and Error DesignStatus codes communicate the broad result of a request: | Class | Examples | Interpretation |
|---|
| 2xx | 200, 201, 204 | Successful processing | | 3xx | 301, 304 | Redirection or cached representation | | 4xx | 400, 401, 403, 404, 409, 429 | Client request or authorization problem | | 5xx | 500, 502, 503, 504 | Server or upstream failure |
Important distinctions include: - 401 Unauthorized means authentication is absent or invalid; it does not mean the user is authenticated but forbidden.
- 403 Forbidden means the server understood the request but refuses to authorize it.
- 404 Not Found means the target resource cannot be found, although some systems intentionally avoid revealing whether a protected resource exists.
- 409 Conflict indicates a conflict with the current state, such as an optimistic-concurrency failure.
- 429 Too Many Requests indicates rate limiting and may include
Retry-After. - 503 Service Unavailable often indicates temporary overload or maintenance.
Error responses should be machine-readable and safe to expose. A useful structure includes a stable error code, human-readable message, field errors where appropriate, and a correlation identifier: {
"error": {
"code": "VALIDATION_FAILED",
"message": "One or more fields are invalid.",
"fields": { "email": "Invalid email format" },
"requestId": "abc-123"
}
}Do not return stack traces, SQL statements, secrets, or personal data to ordinary clients. 6. Pagination, Filtering, and CachingReturning an entire large collection increases response size, memory use, and latency. Pagination divides results into manageable pages. Page-number pagination is simple: GET /api/orders?page=3&pageSize=25
The response should state the items and relevant metadata, such as current page, page size, total count, and links. Offset pagination can become inconsistent when records are inserted while a user is navigating. Cursor pagination uses a stable cursor derived from an ordering key: GET /api/orders?limit=25&after=eyJpZCI6MTAwfQ
Cursor methods are often more stable and efficient for continuously changing or large datasets, but cursors should be opaque and treated as temporary tokens. Filtering, sorting, and field selection should be explicit, bounded, and validated. For example, the server should whitelist sortable fields rather than inserting arbitrary query text into a database statement. Caching can improve performance and reduce server load. HTTP caching uses headers such as Cache-Control, ETag, Last-Modified, and Vary. An ETag identifies a representation. A client can send If-None-Match; if the representation is unchanged, the server can return 304 Not Modified without retransmitting the body. Caching requires a clear freshness policy. Public, immutable assets may be cached for a long period, whereas personalized data may require private or no-store. Incorrectly caching sensitive responses can disclose one user's data to another user. 7. Versioning and EvolutionAPIs evolve as business rules and data models change. Common strategies include URI versioning (/api/v1/products), media-type versioning, and query-parameter versioning. No strategy removes the need for compatibility planning. Prefer additive changes where possible: adding an optional response field is usually safer than renaming or removing a field. Breaking changes may require a new version, a migration period, documentation, and a published retirement date. Servers should monitor usage of old versions and communicate deprecation clearly. Backward compatibility must include semantics, not merely syntax. Changing the meaning of a status code, ordering rule, default page size, or rounding method can break clients even when the JSON shape remains unchanged. 8. GraphQL ConceptsGraphQL is a typed API query language and execution system. A schema defines types, fields, queries, and mutations. Clients request precisely the fields they need: query {
product(id: "42") {
name
price
supplier { name }
}
}Unlike a conventional REST design with multiple resource endpoints, GraphQL commonly exposes one endpoint and allows the client to specify a selection set. Its advantages include strong schema introspection, reduced over-fetching, and flexible composition. Its challenges include query-cost control, authorization at field level, caching complexity, and the possibility of inefficient resolver behavior such as the N+1 query problem. GraphQL errors may appear alongside partial data, so clients must inspect both the data and errors properties. GraphQL is not automatically superior to REST; the choice depends on client diversity, caching needs, domain complexity, team expertise, and operational constraints. 9. Real-Time CommunicationOrdinary HTTP follows a request–response pattern. Real-time requirements include collaborative editing, chat, live dashboards, and notifications. - Polling sends requests periodically. It is simple but may waste requests and introduce delay.
- Long polling holds a request until data is available or a timeout occurs, then the client reconnects.
- Server-Sent Events (SSE) provide a one-way stream from server to browser over HTTP and support automatic reconnection semantics.
- WebSocket creates a persistent, bidirectional channel. It is suitable for interactive applications but requires connection management, authentication, backpressure, heartbeat, and reconnection design.
A real-time system should define event names, payload schemas, ordering expectations, duplicate handling, authorization, and recovery after disconnection. A client should use exponential backoff with a maximum delay rather than reconnecting in a tight loop. Events may need sequence numbers so that a client can detect missed messages and request a replay or refresh. 10. Resilient Error HandlingA reliable web application treats failures as normal possibilities. Resilience includes: - classification: distinguish validation, authentication, authorization, conflict, timeout, cancellation, and server failure;
- bounded timeouts: never wait indefinitely;
- selective retries: retry transient failures, not malformed requests or ordinary authorization failures;
- exponential backoff: increase delay between attempts and add jitter so many clients do not retry simultaneously;
- deduplication: use idempotency keys for retryable state-changing operations;
- fallbacks: show cached data or reduced functionality when appropriate;
- observability: record structured logs, metrics, traces, latency, status codes, and correlation IDs;
- user communication: explain what happened and what action is available.
Retries can amplify an outage. A retry policy should define the maximum number of attempts, eligible status codes, total time budget, and whether the operation is safe to repeat. Circuit breakers may temporarily stop calls to a failing dependency, allowing it to recover and preventing resource exhaustion. Applied Case: Illustrative Campus Event PlatformBackgroundA university builds an event platform where students browse events, register, receive reminders, and view attendance updates. The platform has a browser client, an API gateway, an event service, and a notification service. ProblemDuring registration periods, many students submit the same form repeatedly because the interface appears slow. Large event lists produce long responses, and a notification outage causes registration requests to fail even though registration itself is still possible. AnalysisThe system has three separate concerns: event discovery, registration state, and notifications. Registration is a state-changing operation and must be protected against duplicate submissions. Discovery is read-heavy and suitable for pagination and caching. Notifications are a dependent service and should not determine whether a successful registration is recorded. Approach and SolutionThe API uses GET /api/v1/events?limit=20&after=... with cursor pagination. Public event responses use ETags. Registration uses POST /api/v1/events/{id}/registrations and requires an idempotency key. The server returns 201 Created for a new registration and 409 Conflict if the student is already registered. The client disables duplicate submission while the request is pending, but the server remains responsible for correctness. A registration transaction records an event for the notification service. If notification delivery is temporarily unavailable, the event remains in a durable queue and registration still returns success. The client uses a timeout and displays a clear status message. Real-time attendance updates use SSE; reconnection triggers a refresh if sequence numbers reveal missed events. OutcomePagination reduces response size, ETags avoid repeated transfers, and idempotency prevents duplicate registrations. Decoupled notification processing improves availability. Structured status codes and a documented contract allow frontend and backend teams to test independently. Learning PointsThe case demonstrates that asynchronous programming is only one part of reliability. Correct API semantics, data consistency, caching, queues, observability, and user-centred failure handling must work together. Security, Ethics, and GovernanceAPIs should use TLS, strong authentication, least-privilege authorization, input validation, output encoding, and rate limiting. CORS should permit only required origins and methods. CSRF protections are important when browser credentials are automatically attached. Tokens must not be placed carelessly in URLs or logs. Privacy principles require data minimization, purpose limitation, retention controls, access auditing, and appropriate deletion procedures. Error messages should not reveal whether a sensitive account exists. Logs should avoid passwords, access tokens, and unnecessary personal information. API documentation and automated contract tests should be treated as governance artefacts. Changes require review, security testing, dependency maintenance, and incident procedures. AI-assisted code generation can accelerate API development, but generated code must be reviewed for insecure authorization, hallucinated library behavior, excessive data collection, and missing error paths. Contemporary Best Practices- Define an API contract before implementation and test it in continuous integration.
- Use consistent resource naming, status codes, error shapes, and correlation IDs.
- Validate both incoming requests and outgoing data.
- Prefer concurrency for independent operations, but respect rate limits and dependency capacity.
- Set timeouts and cancellation paths for every network operation.
- Retry only operations and failures for which retrying is safe.
- Make pagination, sorting, and filtering bounded and deterministic.
- Use cache headers deliberately and test authorization boundaries.
- Monitor latency percentiles, error rates, saturation, and dependency health.
- Document deprecations and preserve compatibility during migrations.
Chapter SummaryAsynchronous web applications use the event loop and Promises to perform network work without blocking the user interface. async/await improves readability, while Promise combinators support sequential, parallel, and partial-success workflows. Fetch provides HTTP access but requires explicit status checking, response validation, cancellation, and careful JSON handling. REST APIs are governed by resource-oriented conventions and constraints such as statelessness, cacheability, layered systems, and a uniform interface. API contracts define schemas, authentication, status codes, errors, limits, and evolution rules. Pagination, HTTP caching, versioning, and idempotency improve scalability and compatibility. GraphQL offers typed, client-selected queries but introduces query-cost and caching concerns. SSE and WebSockets support real-time communication, while robust systems require reconnection, ordering, and recovery strategies. Finally, resilience depends on bounded retries, timeouts, observability, security, privacy, and clear user communication. Key TermsAsynchronous programming: Executing work without blocking progress while waiting for external operations. Event loop: A runtime mechanism that schedules queued asynchronous work when the call stack is available. Promise: An object representing the eventual fulfillment or rejection of an asynchronous operation. async/await: JavaScript syntax for writing Promise-based workflows in a sequential style.
Fetch API: A browser and runtime interface for making HTTP requests. JSON: A text-based data-interchange format based on objects, arrays, primitive values, and null. REST: An architectural style based on resources, representations, statelessness, and a uniform interface. API contract: A formal description of requests, responses, errors, security, and operational rules. Idempotency: A property whereby repeating an operation has the same intended effect as performing it once. Pagination: Dividing a collection into smaller result sets. Cursor: An opaque token identifying a position in a paginated collection. ETag: An identifier for a particular representation used in HTTP validation caching. Versioning: Managing incompatible or evolving API contracts over time. GraphQL: A typed query language and execution system for APIs. SSE: Server-Sent Events, a one-way server-to-browser event stream over HTTP. WebSocket: A persistent, bidirectional communication protocol. Backoff: Increasing the delay between repeated attempts after failure. Circuit breaker: A resilience mechanism that temporarily stops calls to a failing dependency. CORS: Cross-Origin Resource Sharing, a browser security mechanism controlling permitted origins. Correlation ID: An identifier used to connect logs and traces belonging to one request or workflow. Review QuestionsShort Answer Questions- What are the three conceptual states of a Promise?
- Why must Fetch clients inspect
response.ok or response.status? - Distinguish HTTP status codes 401 and 403.
- What problem does cursor pagination address?
- State one important difference between SSE and WebSocket.
Long Answer Questions- Explain the event loop, call stack, task queue, and microtask queue in browser JavaScript.
- Discuss REST constraints and explain how they influence API design.
- Compare page-number pagination with cursor pagination, including their advantages and limitations.
- Explain GraphQL's benefits and operational challenges in comparison with REST.
- Describe a comprehensive error-handling strategy for a browser application consuming an unreliable API.
Analytical / Application Questions- Design an API for submitting an online examination. Specify methods, URIs, status codes, idempotency behaviour, and failure handling.
- A search interface sends a request for every keystroke, but responses sometimes arrive out of order. Propose a solution using cancellation, request identity, or both.
- A live dashboard must continue showing useful information during a temporary server outage. Design a caching, retry, fallback, and reconnection strategy and justify each decision.
Multiple Choice Questions with AnswersQuestion 1Which statement best describes a Promise? A. A synchronous loop that blocks the browser B. A representation of the eventual result of an asynchronous operation C. A JSON-only data structure D. A replacement for HTTP status codes Correct Answer: B. A representation of the eventual result of an asynchronous operation Question 2Why does Fetch require explicit checking of response.ok? A. Fetch rejects every response with a 4xx status automatically B. Fetch returns only XML by default C. A fulfilled Fetch Promise can still contain an HTTP error status D. response.ok indicates whether JSON syntax is valid Correct Answer: C. A fulfilled Fetch Promise can still contain an HTTP error status Question 3Which status code most appropriately indicates that a new resource was successfully created? A. 200 B. 201 C. 204 D. 304 Correct Answer: B. 201 Question 4Which operation is generally idempotent? A. POST creating a new order without an idempotency key B. PUT replacing a resource with the same representation C. Sending a chat message through WebSocket D. Submitting a payment form repeatedly Correct Answer: B. PUT replacing a resource with the same representation Question 5What is a principal advantage of cursor pagination for changing, large datasets? A. It always provides an exact total count at no cost B. It avoids the need for an ordering rule C. It can provide more stable traversal than offsets as records change D. It eliminates authorization requirements Correct Answer: C. It can provide more stable traversal than offsets as records change Question 6Which header supports conditional HTTP caching with an entity tag? A. If-None-Match B. Content-Length C. Authorization D. Accept-Language Correct Answer: A. If-None-Match Question 7What does a GraphQL client normally specify in a query? A. Only the server's database table names B. The exact fields it wants in the response C. A separate URL for every selected field D. A WebSocket frame format Correct Answer: B. The exact fields it wants in the response Question 8Which technique is most suitable for one-way server-to-browser event streaming over HTTP? A. SSE B. PUT C. DNS D. CSS polling Correct Answer: A. SSE Question 9Why should retries use backoff and jitter? A. To increase simultaneous load during an outage B. To prevent clients from retrying malformed JSON C. To reduce synchronized retry bursts and give dependencies time to recover D. To convert a 401 response into a 201 response Correct Answer: C. To reduce synchronized retry bursts and give dependencies time to recover Question 10What is the primary purpose of an idempotency key on a state-changing request? A. To compress the request body B. To allow the server to detect repeated attempts safely C. To encrypt a password in the URL D. To replace TLS Correct Answer: B. To allow the server to detect repeated attempts safely
|