DocsHub
Async JavaScript

Async / Await

Learn how to write clean, readable asynchronous JavaScript using async and await.

Async / Await

Promises solved callback hell — but chaining multiple .then() calls still feels different from regular synchronous code. You are always thinking in chains and callbacks.

Async/await is a cleaner syntax built directly on top of Promises. It lets you write async code that looks and reads exactly like synchronous code — top to bottom, line by line — while still being fully non-blocking.

// Promises — chain style
fetchUser(1)
  .then(user => fetchPosts(user.id))
  .then(posts => console.log(posts))
  .catch(error => console.log(error));

// Async/await — reads like normal code
async function loadData() {
  const user = await fetchUser(1);
  const posts = await fetchPosts(user.id);
  console.log(posts);
}

Same thing. Same Promises under the hood. But the async/await version reads like plain English — get the user, then get their posts, then log them.


The async Keyword

Put async before a function to make it an async function.

async function greet() {
  return "Hello!";
}

An async function always returns a Promise — even if you return a plain value. The value gets automatically wrapped in a resolved Promise.

async function greet() {
  return "Hello!";
}

const result = greet();
console.log(result); // Promise { 'Hello!' }

greet().then(value => console.log(value)); // Hello!

This is important — async functions always return Promises. You can use .then() on them if you want — or await them inside another async function.


The await Keyword

await can only be used inside an async function. It pauses the function at that line and waits for the Promise to settle — then resumes with the resolved value.

async function loadUser() {
  const user = await fetchUser(1); // pause here until Promise resolves
  console.log(user.name);          // then continue with the value
}

await does not block the entire program — only the async function it is in. Everything else keeps running normally.

async function loadUser() {
  console.log("Starting...");
  const user = await fetchUser(1); // waits here — but rest of app keeps running
  console.log(`Got user: ${user.name}`);
}

loadUser();
console.log("This runs immediately — does not wait for loadUser");

// Starting...
// This runs immediately — does not wait for loadUser
// Got user: Ali  ← after 1 second

Error Handling With try/catch

With Promises you use .catch(). With async/await you use a regular try/catch block — exactly like synchronous error handling.

async function loadUser(id) {
  try {
    const user = await fetchUser(id);
    console.log("User:", user.name);
  } catch (error) {
    console.log("Error:", error.message);
  }
}

loadUser(1);   // User: Ali
loadUser(-1);  // Error: Invalid user ID.

The try block runs the code. If any awaited Promise rejects — or any error is thrown — execution jumps to the catch block. Clean and familiar — no chain of .catch() calls needed.

finally with async/await

async function loadDashboard(userId) {
  const spinner = document.getElementById("spinner");
  spinner.style.display = "block";

  try {
    const user = await fetchUser(userId);
    const posts = await fetchPosts(user.id);
    renderDashboard(user, posts);
  } catch (error) {
    showError(error.message);
  } finally {
    spinner.style.display = "none"; // always hides spinner
  }
}

finally runs whether the try block succeeded or the catch block ran — perfect for cleanup.


Sequential vs Parallel Execution

This is one of the most important things to understand with async/await — how you write it determines whether operations run one after another or all at once.

Sequential — one after another

async function loadSequential() {
  const user = await fetchUser(1);     // wait 1 second
  const posts = await fetchPosts(1);   // then wait 1 more second
  const settings = await fetchSettings(1); // then wait 1 more second
  // Total: 3 seconds
}

Each await waits for the previous one to finish before starting the next. Total time is the sum of all operations. This is correct when each operation depends on the previous one.

Parallel — all at once

async function loadParallel() {
  const [user, posts, settings] = await Promise.all([
    fetchUser(1),
    fetchPosts(1),
    fetchSettings(1)
  ]);
  // Total: ~1 second — all run simultaneously
}

All three start at the same time. Total time is the slowest one — not the sum of all. Use this when operations are independent of each other.

The common mistake

// ❌ Looks parallel but is actually sequential
async function wrong() {
  const userPromise = fetchUser(1);
  const postsPromise = fetchPosts(1);

  const user = await userPromise;   // waits for user first
  const posts = await postsPromise; // then waits for posts
}

// ✅ Truly parallel — both start before either is awaited
async function correct() {
  const userPromise = fetchUser(1);   // start immediately
  const postsPromise = fetchPosts(1); // start immediately

  const user = await userPromise;   // now wait for results
  const posts = await postsPromise; // already running — minimal wait
}

In the correct version both requests start before either is awaited. By the time you await the second one it has likely already finished. Even better — use Promise.all() for truly parallel operations.


Async Functions Return Promises

Since async functions always return Promises, you can chain them like any other Promise.

async function getUsername(id) {
  const user = await fetchUser(id);
  return user.name; // automatically wrapped in a Promise
}

// Use with .then()
getUsername(1).then(name => console.log(name)); // Ali

// Use with await in another async function
async function displayName() {
  const name = await getUsername(1);
  console.log(name); // Ali
}

Async Arrow Functions

Arrow functions can be async too:

const loadUser = async (id) => {
  const user = await fetchUser(id);
  return user;
};

// Shorter for single expressions
const getName = async (id) => {
  const user = await fetchUser(id);
  return user.name;
};

await Outside Async Functions — Top Level Await

In modern JavaScript modules, you can use await at the top level — outside any function. This is called top-level await.

// In a module file (.mjs or with type="module")
const user = await fetchUser(1);
console.log(user.name);

This only works in ES modules — not in regular scripts. In regular code you still need to wrap everything in an async function.


Async/Await vs Promises — Choosing Between Them

Both are valid. The choice is about readability.

// Promise chain
function loadProfile(userId) {
  return fetchUser(userId)
    .then(user => fetchPosts(user.id)
      .then(posts => ({ user, posts }))
    )
    .then(({ user, posts }) => renderProfile(user, posts))
    .catch(error => showError(error));
}

// Async/await — much easier to follow
async function loadProfile(userId) {
  try {
    const user = await fetchUser(userId);
    const posts = await fetchPosts(user.id);
    renderProfile(user, posts);
  } catch (error) {
    showError(error);
  }
}
PromisesAsync/Await
SyntaxChain of .then()Looks like sync code
Error handling.catch()try/catch
Multiple operationsChainingSequential lines
Parallel operationsPromise.all()await Promise.all()
ReadabilityGoodBetter for complex flows
Under the hoodPromisesPromises

Use async/await for most cases — especially when operations depend on each other. Use Promise methods like Promise.all() inside async functions for parallel operations.


A Real Example — User Profile Page

async function loadProfilePage(userId) {
  const loadingEl = document.getElementById("loading");
  const profileEl = document.getElementById("profile");
  const errorEl = document.getElementById("error");

  loadingEl.style.display = "block";
  profileEl.style.display = "none";
  errorEl.style.display = "none";

  try {
    // Load user and their stats in parallel — they are independent
    const [user, stats] = await Promise.all([
      fetchUser(userId),
      fetchUserStats(userId)
    ]);

    // Load posts sequentially — needs user.id from above
    const posts = await fetchPosts(user.id);

    // Render everything
    profileEl.innerHTML = `
      <div class="profile-header">
        <h1>${user.name}</h1>
        <p>${user.bio ?? "No bio added."}</p>
      </div>
      <div class="profile-stats">
        <span>${stats.followers} followers</span>
        <span>${stats.following} following</span>
        <span>${posts.length} posts</span>
      </div>
      <div class="profile-posts">
        ${posts.map(post => `<div class="post">${post.title}</div>`).join("")}
      </div>
    `;

    profileEl.style.display = "block";

  } catch (error) {
    errorEl.textContent = `Failed to load profile: ${error.message}`;
    errorEl.style.display = "block";
  } finally {
    loadingEl.style.display = "none";
  }
}

loadProfilePage(1);

User and stats load in parallel with Promise.all since they are independent. Posts load after because they need user.id. Everything is wrapped in try/catch/finally for clean error handling and spinner management. Readable top to bottom like synchronous code.


A Real Example — Retry Logic

async function fetchWithRetry(url, retries = 3) {
  for (let attempt = 1; attempt <= retries; attempt++) {
    try {
      const response = await fetch(url);

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

      const data = await response.json();
      console.log(`Success on attempt ${attempt}`);
      return data;

    } catch (error) {
      console.log(`Attempt ${attempt} failed: ${error.message}`);

      if (attempt === retries) {
        throw new Error(`All ${retries} attempts failed.`);
      }

      // Wait before retrying — longer each time
      await new Promise(resolve => setTimeout(resolve, attempt * 1000));
    }
  }
}

// Usage
try {
  const data = await fetchWithRetry("https://api.example.com/data");
  console.log(data);
} catch (error) {
  console.log("Final failure:", error.message);
}

A loop retries the request up to 3 times. Each failure waits a bit longer before the next attempt — 1 second, then 2 seconds, then 3. If all attempts fail, a final error is thrown. This pattern is used in production APIs constantly.


Summary

  • async before a function makes it return a Promise automatically
  • await pauses an async function until a Promise settles — then resumes with the value
  • await can only be used inside async functions
  • Use try/catch for error handling — just like synchronous code
  • Use finally for cleanup that always runs
  • await operations run sequentially by default — each waits for the previous
  • Use await Promise.all([...]) to run independent operations in parallel
  • Async/await is built on Promises — both are valid, async/await is preferred for readability
  • Always think about whether operations are dependent (sequential) or independent (parallel) — it impacts performance significantly

On this page