JavaScript async/await: Complete Tutorial Guide

Master async/await in JavaScript from the ground up. Learn how async functions return Promises, how await suspends execution, error handling with try/catch, parallel patterns, and common pitfalls to avoid.

JavaScriptintermediate
14 min read

async/await is the most readable syntax for writing asynchronous JavaScript. Introduced in ES2017, it lets you write async code that looks and reads like synchronous code, while remaining non-blocking. Under the hood it is syntax sugar over Promises — every async function returns a Promise, and await pauses the function until a Promise settles.

The async Keyword

Marking a function with async does two things:

  1. The function always returns a Promise
  2. You can use await inside it
javascriptjavascript
// Synchronous function
function add(a, b) {
  return a + b;
}
console.log(add(1, 2)); // 3
 
// async function: return value is wrapped in a Promise
async function addAsync(a, b) {
  return a + b;
}
addAsync(1, 2).then(console.log); // 3
 
// If you explicitly return a Promise, it is NOT double-wrapped
async function fetchUser(id) {
  return fetch(`/api/users/${id}`).then((r) => r.json());
  // Returns the same Promise — not a Promise<Promise<...>>
}

Async Functions Always Return a Promise

javascriptjavascript
async function example() {
  return 42;
}
 
const result = example();
console.log(result instanceof Promise); // true
console.log(result); // Promise { 42 }
 
result.then((v) => console.log(v)); // 42

This means async functions are compatible with all Promise APIs — you can .then(), .catch(), await, and pass them to Promise.all.

The await Keyword

await pauses the execution of the surrounding async function until the Promise settles, then returns the resolved value:

javascriptjavascript
async function loadUser(id) {
  // Execution pauses here until fetchUser resolves
  const user = await fetchUser(id);
 
  // This line runs only after fetchUser resolves
  console.log(user.name);
 
  return user;
}

What await Actually Does

await promise is equivalent to promise.then(continueHere). The function does not block the thread — control returns to the caller and the rest of the function runs as a microtask once the Promise settles:

javascriptjavascript
console.log("before call");
 
async function step() {
  console.log("before await");
  const result = await Promise.resolve("done");
  console.log("after await:", result); // Runs as microtask
}
 
step();
console.log("after call");
 
// Output:
// "before call"
// "before await"
// "after call"          ← main thread continues synchronously
// "after await: done"   ← microtask runs after synchronous code

For deep details on why this ordering occurs, see the JavaScript event loop and microtasks vs macrotasks.

Awaiting Non-Promise Values

await can be used with any value. Non-Promise values are wrapped in Promise.resolve() first:

javascriptjavascript
async function demo() {
  const a = await 42;          // await 42 = await Promise.resolve(42)
  const b = await "hello";     // Immediate
  const c = await null;        // null
  console.log(a, b, c);       // 42 "hello" null
}

Error Handling

If an awaited Promise rejects, it throws at the point of the await expression. Use try/catch:

javascriptjavascript
async function loadDashboard(userId) {
  try {
    const user = await fetchUser(userId);
    const posts = await fetchPosts(user.id);
    return { user, posts };
  } catch (error) {
    // Catches rejection from fetchUser OR fetchPosts
    console.error("Dashboard load failed:", error.message);
    return null;
  }
}

Granular Error Handling

For different handling per operation, use nested try/catch:

javascriptjavascript
async function initApp() {
  let config;
  try {
    config = await loadConfig();
  } catch {
    config = defaultConfig; // Fall back silently
  }
 
  let user;
  try {
    user = await authenticateUser();
  } catch (err) {
    redirectToLogin();
    return; // Stop initialization
  }
 
  renderApp(config, user);
}

Inline Error Handling With .catch()

Since async functions return Promises, you can use .catch() directly on the call:

javascriptjavascript
async function fetchData(url) {
  const response = await fetch(url);
  return response.json();
}
 
// Option 1: try/catch inside
async function useData() {
  try {
    const data = await fetchData("/api/data");
    process(data);
  } catch (err) {
    handleError(err);
  }
}
 
// Option 2: .catch() at the call site
fetchData("/api/data")
  .then(process)
  .catch(handleError);
 
// Option 3: per-await .catch() for inline fallback
async function useDataWithFallback() {
  const data = await fetchData("/api/data").catch(() => defaultData);
  process(data); // data is never undefined
}

Parallel vs Sequential Execution

The most common async/await performance mistake is accidentally making parallel operations sequential:

javascriptjavascript
// SLOW: sequential — each waits for the previous
async function loadProfileSlow(userId) {
  const user = await fetchUser(userId);     // 200ms
  const posts = await fetchPosts(userId);   // 300ms
  const friends = await fetchFriends(userId); // 150ms
  // Total: 650ms
  return { user, posts, friends };
}
 
// FAST: parallel — all run simultaneously
async function loadProfileFast(userId) {
  const [user, posts, friends] = await Promise.all([
    fetchUser(userId),     // \
    fetchPosts(userId),    //  } all start at the same time
    fetchFriends(userId),  // /
  ]);
  // Total: 300ms (slowest one)
  return { user, posts, friends };
}
 
// CORRECT approach: use sequential when there's a dependency
async function loadProfileWithDependency(userId) {
  const user = await fetchUser(userId);          // Must come first
  const [posts, friends] = await Promise.all([  // These two are independent
    fetchPosts(user.id),
    fetchFriends(user.id),
  ]);
  return { user, posts, friends };
}

Rule: use await in sequence only when the result of one is needed to make the next call. For independent operations, use Promise.all.

Common Pitfalls

Pitfall 1: await in Loops

javascriptjavascript
const ids = [1, 2, 3, 4, 5];
 
// WRONG: sequential — each fetch waits for the previous
async function fetchAllSlow(ids) {
  const results = [];
  for (const id of ids) {
    const data = await fetchItem(id); // Each waits!
    results.push(data);
  }
  return results;
}
 
// CORRECT: parallel with Promise.all
async function fetchAllFast(ids) {
  return Promise.all(ids.map((id) => fetchItem(id)));
}
 
// ALSO CORRECT: for-await with serial processing when order matters
async function processInOrder(ids) {
  const results = [];
  for (const id of ids) {
    const data = await fetchItem(id);
    results.push(await processItem(data)); // Must be sequential
  }
  return results;
}

Pitfall 2: Forgetting await

javascriptjavascript
async function saveUser(user) {
  await db.save(user); // Correctly awaited
}
 
async function handleRequest() {
  saveUser(updatedUser); // Missing await! Runs in background
  sendResponse(200);     // Sends response before save might complete
}
 
// Fix:
async function handleRequestFixed() {
  await saveUser(updatedUser);
  sendResponse(200);
}

Pitfall 3: await in Non-async Callbacks

await only works directly inside an async function. It does NOT work inside regular callbacks:

javascriptjavascript
// WRONG: await inside a non-async callback
async function processItems(items) {
  items.forEach((item) => {
    const result = await processItem(item); // SyntaxError!
    log(result);
  });
}
 
// CORRECT: make the callback async
async function processItems(items) {
  items.forEach(async (item) => {    // async callback
    const result = await processItem(item); // Works, but...
    log(result);                            // forEach does not await these!
  });
}
 
// ACTUALLY CORRECT: use Promise.all + .map()
async function processItems(items) {
  await Promise.all(
    items.map(async (item) => {
      const result = await processItem(item);
      log(result);
    })
  );
}

Async Function Signatures

Async can be applied to any function form:

javascriptjavascript
// Function declaration
async function fetchData() { ... }
 
// Function expression
const fetchData = async function () { ... };
 
// Arrow function
const fetchData = async () => { ... };
 
// Method
const api = {
  async fetchData() { ... },
};
 
// Class method
class DataService {
  async fetchData() { ... }
}
Rune AI

Rune AI

Key Insights

  • async functions always return Promises: Even return 42 inside an async function produces a fulfilled Promise — you cannot escape the async context once inside it
  • await is not blocking: It yields control back to the event loop, resuming the function in a microtask when the awaited Promise settles
  • Use Promise.all for independent operations: Sequential await on unrelated operations is a performance anti-pattern — start them all with Promise.all and await the array
  • try/catch replaces .catch(): An awaited rejection throws at the await point, making standard synchronous-style error handling work correctly in async functions
  • await inside forEach does not wait: Use Promise.all(array.map(async ...)) to correctly parallelize async operations over an array
RunePowered by Rune AI

Frequently Asked Questions

Can await be used at the top level?

Yes, since ES2022, top-level `await` is supported in ES modules (files with `import`/`export`). It is not available in CommonJS modules (`require`/`module.exports`). In older environments, wrap top-level code in an `async` IIFE: ```javascript (async () => { const data = await fetchInitialData(); init(data); })(); ```

Does await block the JavaScript thread?

No. `await` pauses only the surrounding async function, not the main thread. The rest of the program (other event handlers, UI updates) continues running normally. The paused function resumes when its awaited Promise settles, as a microtask in the event loop.

What happens if I await a Promise that never resolves?

The async function pauses indefinitely at that `await`. It will never proceed past that point and never return. This can cause memory leaks if the async function has references to DOM elements or large objects. Always use a timeout wrapper for external calls that might hang.

Is async/await always better than .then() chaining?

For sequential logic, async/await is almost always more readable. For parallel composition (`Promise.all`, `Promise.race`) and complex chaining with multiple `.catch()` recovery points, mixing `.then()` with async/await is often cleaner than forcing everything into try/catch blocks with async/await.

Can I use async/await with older browser APIs like XMLHttpRequest?

Older callback-based APIs must be wrapped in Promises first, then you can await them. See [converting promises to async/await](/tutorials/programming-languages/javascript/converting-promises-to-async-await-in-javascript) for patterns on wrapping legacy APIs.

Conclusion

async/await is built entirely on Promises — every async function returns a Promise, and await is a microtask-based pause. It makes sequential async code read naturally while preserving the full power of the Promise API for parallel operations. The most important discipline is knowing when to use await serially versus Promise.all in parallel — this decision has a direct impact on performance and correctness.