Tutorials Logic, IN info@tutorialslogic.com

Async/Await in AJAX Parallel Requests

Awaited Request Flow

An async function always returns a Promise. Inside it, await pauses only that function until the awaited value settles; it does not block the browser main thread. The fulfilled value becomes the result of the await expression, while a rejected Promise behaves like a thrown error and can be handled with try and catch.

The hard part is not the syntax. A reliable AJAX workflow must check HTTP status, decide which requests truly depend on earlier results, connect concurrent promises immediately, cancel obsolete work, and preserve enough error context for the caller to choose a retry, fallback, or user message.

Fetch with Await

fetch() returns a Promise that fulfills with a Response after response headers arrive. A 404 or 500 is still a fulfilled Fetch promise, so await fetch(url) does not automatically throw for an HTTP error. Check response.ok or response.status before parsing the body.

Response body readers such as json() are asynchronous too. Parse only after checking the status, and remember that a body can be empty or invalid even when its Content-Type claims JSON. Let low-level request helpers throw a useful error; let the UI boundary decide what the learner or user should see.

Basic async/await with Fetch

Basic async/await with Fetch
// async function - always returns a Promise
async function getUser(id) {
  // try-catch replaces .catch() for error handling
  try {
    const response = await fetch(`/api/users/${id}`);

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

    const user = await response.json(); // await the body parsing too
    return user;

  } catch (error) {
    console.error('getUser failed:', error.message);
    throw error; // re-throw so callers can handle it
  } finally {
    // finally always runs - good for cleanup
    console.log('getUser request complete');
  }
}

// Call the async function
getUser(1)
  .then(user => console.log('User:', user.name))
  .catch(err => console.error('Caught:', err.message));

Sequential or Concurrent

Use sequential awaits when request B needs data produced by request A. When requests are independent, start them together and await Promise.all(). Sequential independent fetches add their waiting times; concurrent fetches overlap them.

Promise.all() rejects when any input rejects, but it connects every input to the combined chain immediately. Promise.allSettled() waits for every input and returns a status for each, which is useful when a dashboard can show partial results. Neither method cancels remaining network requests automatically.

Sequential vs Parallel Requests

Sequential vs Parallel Requests
// ---- SEQUENTIAL - each request waits for the previous ----
// Total time = sum of all request times (slow!)
async function loadSequential() {
  const user    = await fetch('/api/users/1').then(r => r.json());
  const posts   = await fetch('/api/posts?userId=1').then(r => r.json());
  const profile = await fetch('/api/profile/1').then(r => r.json());
  // Runs in series: user -> posts -> profile
  return { user, posts, profile };
}

// ---- PARALLEL with Promise.all - all requests fire simultaneously ----
// Total time = longest single request (fast!)
async function loadParallel() {
  const [user, posts, profile] = await Promise.all([
    fetch('/api/users/1').then(r => r.json()),
    fetch('/api/posts?userId=1').then(r => r.json()),
    fetch('/api/profile/1').then(r => r.json())
  ]);
  // All three requests run concurrently
  return { user, posts, profile };
}

// ---- Promise.allSettled - get results even if some fail ----
async function loadWithFallback() {
  const results = await Promise.allSettled([
    fetch('/api/users/1').then(r => r.json()),
    fetch('/api/posts?userId=1').then(r => r.json()),
    fetch('/api/missing-endpoint').then(r => r.json()) // this will fail
  ]);

  results.forEach((result, i) => {
    if (result.status === 'fulfilled') {
      console.log(`Request ${i} succeeded:`, result.value);
    } else {
      console.warn(`Request ${i} failed:`, result.reason.message);
    }
  });
}

Cancellation and Stale Results

Search boxes and route changes can make an earlier request obsolete. AbortController provides a signal that Fetch observes. Abort the previous controller before starting a replacement, and treat AbortError as expected control flow rather than showing a failure banner.

Cancellation does not guarantee the server stopped processing after it received the request. State-changing endpoints still need server-side authorization, validation, transactions, and idempotency where retries are possible.

Cancel the Previous Search

Cancel the Previous Search
let activeController;

async function searchProducts(query) {
  activeController?.abort();
  activeController = new AbortController();

  try {
    const response = await fetch(`/api/products?q=${encodeURIComponent(query)}`, {
      signal: activeController.signal,
    });
    if (!response.ok) throw new Error(`Search failed: ${response.status}`);
    return await response.json();
  } catch (error) {
    if (error.name === "AbortError") return [];
    throw error;
  }
}

Error Ownership

Catch an error where you can add context, recover, or translate it into a user-facing state. A helper that logs and silently returns undefined forces every caller to fail later with a less useful message. Either return a valid fallback with a documented meaning or rethrow after adding context.

Use finally for cleanup that must run on success and failure, such as clearing a loading indicator. Do not put the success-only render path in finally. For retries, limit attempts, delay between them, and retry only transient operations that are safe to repeat.

  • Network rejection and HTTP failure are different conditions.
  • JSON parsing can fail independently of the request status.
  • Promise.all is fail-fast; allSettled preserves each outcome.
  • Abort expected obsolete work instead of letting stale results overwrite newer UI.

Async IIFE and Top-Level Await

Async IIFE and Top-Level Await
// Async IIFE - run async code at the top level (pre-ES2022)
(async () => {
  try {
    const res = await fetch('/api/config');
    const config = await res.json();
    console.log('App config loaded:', config);
    initApp(config);
  } catch (err) {
    console.error('Failed to load config:', err);
  }
})();

// Top-level await (ES2022, requires type="module")
// <script type="module">
//   const res = await fetch('/api/config');
//   const config = await res.json();
//   initApp(config);
// </script>

// Error propagation through async call chains
async function step1() {
  const data = await fetch('/api/step1').then(r => r.json());
  return data.value;
}

async function step2(value) {
  const data = await fetch(`/api/step2?v=${value}`).then(r => r.json());
  return data.result;
}

async function runPipeline() {
  try {
    const v1 = await step1();
    const v2 = await step2(v1);
    console.log('Pipeline result:', v2);
  } catch (err) {
    // Catches errors from step1 OR step2
    console.error('Pipeline failed:', err.message);
  }
}

Real-World Async/Await Patterns

Real-World Async/Await Patterns
// Pattern 1: Loading state management
async function loadAndRender(url, containerId) {
  const container = document.getElementById(containerId);
  container.innerHTML = '<div class="spinner">Loading...</div>';

  try {
    const res = await fetch(url);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    const data = await res.json();
    container.innerHTML = renderData(data);
  } catch (err) {
    container.innerHTML = `<p class="error">${err.message}</p>`;
  }
}

// Pattern 2: Polling - check for updates every N seconds
async function pollForStatus(jobId, intervalMs = 2000) {
  while (true) {
    const res = await fetch(`/api/jobs/${jobId}`);
    const job = await res.json();

    if (job.status === 'completed') {
      console.log('Job done:', job.result);
      break;
    } else if (job.status === 'failed') {
      throw new Error('Job failed: ' + job.error);
    }

    // Wait before next poll
    await new Promise(resolve => setTimeout(resolve, intervalMs));
  }
}

// Pattern 3: Race - use whichever request finishes first
async function fetchWithFallback(primaryUrl, fallbackUrl) {
  try {
    return await Promise.race([
      fetch(primaryUrl).then(r => r.json()),
      new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 3000))
    ]);
  } catch {
    console.warn('Primary failed, using fallback');
    return fetch(fallbackUrl).then(r => r.json());
  }
}
Before you move on

Async Request Review

5 checks
  • Check response.ok before treating a Fetch response as success.
  • Await body parsing and handle malformed or empty bodies deliberately.
  • Run only independent requests concurrently with Promise.all or allSettled.
  • Abort stale requests when newer UI state makes them irrelevant.
  • Catch errors at the layer that can recover or present meaningful context.

Async Control Mistakes

  • Assuming Fetch throws on 404

    Check response.ok and throw an application error before parsing.
  • Awaiting independent requests one by one

    Create the promises together and await Promise.all().
  • Swallowing the rejection

    Return a valid documented fallback or rethrow so the caller can handle failure.

Try this next

Exercise the Promise Paths

0 of 3 completed

  1. Return parsed JSON for 2xx responses and throw an error containing status and URL otherwise.
  2. Run three independent timers sequentially and with Promise.all, then explain the elapsed-time difference.
  3. Use AbortController so only the latest query is allowed to update the result list.

Await and Fetch Questions

No. It suspends the surrounding async function while other tasks can continue; CPU-heavy synchronous work still blocks.

It observes promises that should already represent started work. JavaScript remains single-threaded for normal code while network operations can overlap.

Use it when every outcome matters and partial success is useful, such as independent dashboard panels.

Next Step
Next Practice

Finish the concept here, then reinforce it with hands-on coding, interview prep, or a tool that matches the topic.

Browse Free Tutorials

Explore 500+ free tutorials across 20+ languages and frameworks.