Advanced Web Programming

Asynchronous JavaScript, AJAX, Fetch and Axios

PGCP-AC

Network requests, timers, user interaction and file operations finish later than the code that starts them. JavaScript represents this delayed completion with callbacks and promises. Reliable applications must also model loading, failure, cancellation, concurrency and stale results.

1. Completion happens later

Asynchronous operations allow a program to continue while work such as network I/O is pending. A callback describes work to perform later. A promise represents eventual fulfillment or rejection and can settle only once. then transforms fulfilled results; catch handles rejection; finally supports completion work without being the primary place to transform a result. An async function always returns a promise. await suspends that function's continuation, not the entire JavaScript runtime.

The event loop schedules work after the current call stack has cleared. Promise reactions use the microtask mechanism; timers schedule callbacks for later processing and do not guarantee exact timing. Long synchronous computations still block the executing JavaScript thread. Promise.all collects successful results in input order and rejects if an input rejects; this does not automatically cancel the remaining operations.

2. Network exchanges

AJAX updates part of a page using a request without requiring a full page reload. Despite the name, it can exchange JSON or text rather than XML. XMLHttpRequest is the traditional API. Fetch returns a promise for a Response; HTTP error statuses ordinarily still fulfill that promise, so check ok or status. Reading JSON from the response is a separate asynchronous step. Axios is another promise-based client and, by default, rejects responses outside its accepted success-status range.

async function loadItems() {
  const response = await fetch('/api/items');
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return await response.json();
}

Cross-origin browser access is controlled by the same-origin policy and CORS response headers. Client-side code cannot grant itself permission by adding an arbitrary response header to a request. Handle loading, empty results, failure and stale responses explicitly so that users do not mistake old information for the latest response.

3. Sequential and concurrent work

const [books, authors] = await Promise.all([
  fetch('/api/books').then(r => {
    if (!r.ok) throw new Error('Books request failed');
    return r.json();
  }),
  fetch('/api/authors').then(r => {
    if (!r.ok) throw new Error('Authors request failed');
    return r.json();
  })
]);

These independent requests can be in progress together. In contrast, a second request that needs an identifier from the first must wait for that result. Promise.allSettled records each outcome when partial success is useful. With Axios, a configured instance can share a base URL, timeout and interceptors across related requests. Its response wrapper contains both data and transport metadata. Error handling should distinguish an HTTP error response, failure to receive a response and failure while setting up the request.

4. Call stack, tasks and microtasks

JavaScript runs one stack of synchronous calls in an execution agent. Browser facilities can perform waiting outside that stack and later queue callbacks. The event loop chooses queued work only after the current stack becomes empty.

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

The script prints A and D synchronously. The promise reaction is a microtask and runs after the stack clears but before the next timer task. A zero-millisecond delay means “eligible after at least this delay,” not “execute immediately.” Rendering and other queued work also influence timing.

Asynchrony does not make heavy computation nonblocking. A long loop still occupies the thread and delays input, rendering, timers and promise reactions. Break work into suitable chunks or use a worker when CPU-bound processing must occur away from the main thread.

5. Promise states and chaining

A promise is pending and then settles exactly once as fulfilled or rejected. Later attempts to settle it have no effect. then returns a new promise, which enables transformation and error propagation.

fetch('/api/profile')
  .then(response => {
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return response.json();
  })
  .then(profile => renderProfile(profile))
  .catch(error => showError(error))
  .finally(() => hideSpinner());

Returning a value fulfills the next promise with that value. Throwing rejects it. Returning a promise makes the chain adopt that promise's eventual outcome. Forgetting return inside a block-bodied callback can cause the following step to receive undefined and run too early.

catch is rejection handling for the chain above it. finally runs for either outcome and normally passes the original outcome through, making it useful for cleanup rather than result conversion.

6. Async functions and error flow

An async function always returns a promise. A returned ordinary value becomes fulfillment; a thrown exception becomes rejection. await pauses that async function's continuation until the awaited value is available, without blocking the entire runtime.

async function loadProfile(id) {
  const response = await fetch(`/api/profiles/${encodeURIComponent(id)}`);
  if (!response.ok) throw new Error(`Profile request failed: ${response.status}`);
  return response.json();
}

try {
  const profile = await loadProfile('A 17');
  renderProfile(profile);
} catch (error) {
  showError(error);
}

Async/await changes syntax, not the underlying promise semantics. Sequential awaits are correct when a later request depends on an earlier result, but they unnecessarily serialize independent work.

7. AJAX and XMLHttpRequest

AJAX describes background HTTP communication that updates part of a page without a full navigation. The payload may be JSON, text, HTML, XML or binary data. XMLHttpRequest is the historical browser API and exposes lifecycle events, response types, progress facilities and cancellation.

Existing XHR code remains important, especially where upload progress is required. Newer code often uses Fetch because it integrates naturally with promises and request/response objects. API choice does not eliminate the need to interpret HTTP status, validate response data and update the interface safely.

8. Fetch request and response handling

Fetch rejects for failures such as network errors or cancellation. It normally fulfills when an HTTP response arrives, even when the status is 404 or 500.

async function requestJSON(url, options = {}) {
  const response = await fetch(url, {
    ...options,
    headers: { Accept: 'application/json', ...options.headers }
  });

  if (!response.ok) {
    const detail = await response.text();
    throw new Error(`HTTP ${response.status}: ${detail}`);
  }

  if (response.status === 204) return null;
  return response.json();
}

Response body readers such as .json(), .text() and .blob() are asynchronous and generally consume the body stream. A successful status does not guarantee valid JSON. Parsing can fail separately and parsed data can still violate the application schema.

9. Sending JSON

const created = await requestJSON('/api/tasks', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ title: 'Review promises' })
});

Content-Type describes the request body being sent; Accept describes desired response representations. JSON.stringify is necessary because a request body cannot infer a JSON wire representation from an arbitrary plain object.

Credentials, cookies, CSRF protection, authorization headers and retry behavior depend on the API contract. Do not store or expose long-lived secrets in client source.

10. Axios behavior

Axios is a promise-based client with request configuration, automatic response-data transformation, interceptors and configurable status validation.

const api = axios.create({ baseURL: '/api', timeout: 8000 });

async function loadTasks() {
  const response = await api.get('/tasks');
  return response.data;
}

By default, Axios rejects responses outside its accepted success range, unlike Fetch's fulfilled HTTP-error response. Axios errors distinguish a received error response, a request without a response and setup failure. Interceptors can centralize cross-cutting behavior, but must avoid swallowing errors, retry loops and duplicate registration.

11. Promise coordination

OperationBehavior
Promise.allFulfills with results in input order; rejects when one input rejects
Promise.allSettledWaits for every input and reports each outcome
Promise.raceSettles with the first input to settle
Promise.anyFulfills with the first fulfillment; rejects if all reject

Use Promise.all for independent requirements that must all succeed. Its rejection does not cancel operations already started. Use allSettled when partial results and a complete outcome report are useful. Apply concurrency limits when a large collection would overload the browser, network or server.

12. Cancellation and timeouts

async function loadWithTimeout(url, milliseconds) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), milliseconds);
  try {
    return await requestJSON(url, { signal: controller.signal });
  } finally {
    clearTimeout(timer);
  }
}

AbortController signals cancellation to Fetch and other compatible operations. Cancellation is cooperative: the caller must pass the signal and application code must classify an abort separately when it is an expected outcome. Clear timeout resources after either success or failure.

13. Race conditions and stale responses

Search interfaces can issue request B after request A but receive A last. Without protection, the old result replaces the current one.

let currentController;

async function search(query) {
  currentController?.abort();
  currentController = new AbortController();
  const result = await requestJSON(`/api/search?q=${encodeURIComponent(query)}`, {
    signal: currentController.signal
  });
  renderResults(result);
}

Cancellation reduces unnecessary work. A request sequence number is another solution: render only when the completing request still has the newest identifier. Debouncing controls how often requests start but does not by itself prevent an earlier response from arriving last.

14. CORS and same-origin policy

Browser scripts are restricted from reading cross-origin responses unless the response grants suitable access. The server supplies CORS response headers. The client cannot grant itself permission by inventing Access-Control-Allow-Origin in its request.

Some cross-origin requests trigger a preflight OPTIONS exchange before the actual request. CORS is enforced by browsers and is not an authentication or authorization system. A server must still authenticate callers and protect its data even if nonbrowser clients are not subject to browser CORS enforcement.

15. User-interface state

A request-driven component should distinguish at least:

  • idle, before a request;
  • loading, while work is pending;
  • success with data;
  • success with an empty result;
  • failure with a recoverable explanation;
  • canceled or superseded work where that state matters.

Disable only controls that would cause harmful duplication, keep navigation usable, preserve previous useful data when appropriate and announce significant changes accessibly. An endless spinner is not error handling.

16. Integrated loader

async function refreshDashboard() {
  setStatus('loading');
  try {
    const [courses, notices] = await Promise.all([
      requestJSON('/api/courses'),
      requestJSON('/api/notices')
    ]);
    renderDashboard({ courses, notices });
    setStatus(courses.length || notices.length ? 'ready' : 'empty');
  } catch (error) {
    if (error.name !== 'AbortError') {
      console.error(error);
      setStatus('error');
    }
  }
}

The requests start together because they are independent. The interface represents pending, empty, ready and error states. Production code may add cancellation, schema validation, authentication handling and carefully bounded retries.

17. Practical considerations

  1. An async function always returns a promise.
  2. Await pauses one async continuation, not the whole runtime.
  3. Promise reactions run after current synchronous code.
  4. A timer delay is a minimum scheduling threshold, not an exact time.
  5. Fetch normally fulfills for HTTP 404 or 500; check ok.
  6. response.json() returns a promise and can reject during parsing.
  7. Promise.all preserves input order, not completion order.
  8. Aggregate rejection does not cancel other operations.
  9. CORS permission comes from the response/server policy.
  10. Debouncing alone does not solve stale-response races.

Continue learning

Related notes

Put this topic into timed practice

Open mock tests when you want full-exam pacing, or keep drilling in practice mode.