Why Asynchronous Code Is Tricky

JavaScript runs on a single thread. When you make a network request, read a file, or set a timer, you can't just pause everything and wait — you need a way to say "do this now, and when it's done, do that." Promises and async/await are the two modern tools for handling exactly this problem.

What Is a Promise?

A Promise is an object that represents the eventual result of an asynchronous operation. It can be in one of three states:

  • Pending — the operation hasn't completed yet
  • Fulfilled — the operation completed successfully, with a value
  • Rejected — the operation failed, with a reason (error)

You interact with a Promise using .then() for success and .catch() for errors:

fetch('/api/user')
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error(error));

This is readable for simple chains, but nesting multiple .then() calls for dependent operations can quickly become hard to follow — sometimes called "Promise chains" that are difficult to debug.

What Is Async/Await?

async/await is syntactic sugar built on top of Promises. It doesn't replace them — it gives you a way to write asynchronous code that reads like synchronous code.

  • Mark a function with async to make it return a Promise automatically.
  • Use await inside that function to pause execution until a Promise resolves.
async function getUser() {
  try {
    const response = await fetch('/api/user');
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error(error);
  }
}

The logic is identical to the Promise chain above, but many developers find this structure easier to reason about, especially when multiple async calls depend on each other.

Key Differences at a Glance

Aspect Promises (.then/.catch) Async/Await
Syntax style Chained methods Linear, synchronous-looking
Error handling .catch() try/catch
Parallel execution Promise.all() — natural fit Need Promise.all() explicitly
Debugging Stack traces can be tricky Cleaner stack traces
Readability Good for short chains Better for complex flows

When Promises Are Still the Better Choice

Despite the popularity of async/await, raw Promises are often cleaner for:

  • Running multiple requests in parallel: Promise.all([fetch(a), fetch(b)]) is concise and clear.
  • Short, one-off chains that don't benefit from extra function wrapping.
  • Promise combinators like Promise.allSettled(), Promise.race(), and Promise.any().

A Common Mistake to Avoid

A frequent error with async/await is accidentally running requests sequentially when they could run in parallel:

// ❌ Sequential — slower
const user = await fetchUser();
const posts = await fetchPosts();

// ✅ Parallel — faster
const [user, posts] = await Promise.all([fetchUser(), fetchPosts()]);

The Bottom Line

Use async/await as your default — it's more readable and easier to maintain. Reach for Promise methods directly when you need parallel execution or combinators. Since async/await is built on Promises, understanding both makes you a more capable JavaScript developer.