</>StackKit
</>StackKit

Developer tutorials & guides

JavaScript async code
JavaScript

JavaScript Async/Await: The Complete Guide

Master asynchronous JavaScript with async/await. Learn how Promises work under the hood, error handling patterns, parallel execution, and common pitfalls.

A
Alex Carter
April 12, 20258 min read
#javascript#async#promises#tutorial

Why Async/Await Exists

JavaScript is single-threaded — only one thing runs at a time. But many operations (HTTP requests, file reads, database queries) take time. Async/await gives you a way to wait for those operations without blocking the entire thread.

Under the hood, async/await is syntax sugar over Promises. Understanding Promises makes async/await fully transparent.


Promises Refresher

A Promise represents a value that may not be available yet:

const promise = new Promise((resolve, reject) => {
  setTimeout(() => resolve('done!'), 1000);
});

promise
  .then(value => console.log(value))  // "done!" after 1s
  .catch(error => console.error(error));

async/await Syntax

async marks a function as asynchronous. Inside it, await pauses execution until the Promise resolves:

async function fetchUser(id) {
  const res = await fetch(`/api/users/${id}`);
  const user = await res.json();
  return user;
}

This reads like synchronous code but runs asynchronously. Calling fetchUser returns a Promise.


Error Handling

try/catch — The Standard Way

async function getUser(id) {
  try {
    const res = await fetch(`/api/users/${id}`);
    if (!res.ok) throw new Error(`HTTP error: ${res.status}`);
    return await res.json();
  } catch (error) {
    console.error('Failed to fetch user:', error);
    return null;
  }
}

Wrapping Promises (Utility Pattern)

async function safe(promise) {
  try {
    const data = await promise;
    return [null, data];
  } catch (error) {
    return [error, null];
  }
}

const [error, user] = await safe(fetchUser(id));
if (error) return handleError(error);

Sequential vs Parallel Execution

Sequential (each waits for the previous)

// Total time: 1s + 1s + 1s = 3 seconds
const user = await fetchUser(id);
const posts = await fetchPosts(user.id);
const comments = await fetchComments(posts[0].id);

Parallel (all start at the same time)

// Total time: max(1s, 1s, 1s) = 1 second
const [user, settings, notifications] = await Promise.all([
  fetchUser(id),
  fetchSettings(id),
  fetchNotifications(id),
]);

Use Promise.all whenever operations are independent. This is one of the biggest performance wins in async code.


Promise.allSettled — When You Need All Results

Promise.all rejects if any Promise rejects. Promise.allSettled waits for all, regardless:

const results = await Promise.allSettled([
  fetchUser(1),
  fetchUser(2),  // might fail
  fetchUser(3),
]);

results.forEach(result => {
  if (result.status === 'fulfilled') {
    console.log(result.value);
  } else {
    console.error(result.reason);
  }
});

Promise.race — First to Resolve Wins

Useful for timeouts:

function withTimeout(promise, ms) {
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error('Timeout')), ms)
  );
  return Promise.race([promise, timeout]);
}

const user = await withTimeout(fetchUser(id), 5000);

Common Mistakes

Forgetting await:

// Bug: user is a Promise, not the resolved value
const user = fetchUser(id);
console.log(user.name); // undefined

Awaiting in a loop sequentially when parallel would work:

// Slow
for (const id of ids) {
  await processUser(id); // one at a time
}

// Fast
await Promise.all(ids.map(id => processUser(id)));

Conclusion

Async/await is built on Promises — never forget that. Master the three critical patterns: sequential awaits for dependent operations, Promise.all for independent parallel operations, and try/catch for errors. These three patterns cover nearly every real-world async scenario you'll encounter.

#javascript#async#promises#tutorial