JavaScript Promises vs async/await — Deep Comparison

JavaScript Promises vs async/await deep dive: error handling, Promise.all, Promise.race, concurrency patterns, and when to use each approach with practical examples.

Understanding the Event Loop First

JavaScript is single-threaded — it can only execute one thing at a time. Asynchronous operations (network requests, timers, file I/O) are handled by the browser's Web APIs or Node.js's libuv, which push callbacks onto the microtask queue when complete. Promises and async/await are different syntactic layers on top of this same event loop — they don't add concurrency, they add readable control flow for asynchronous operations.

Promises: The Foundation

A Promise represents a value that may be available now, in the future, or never. It has three states: pending, fulfilled, or rejected.

// Creating a Promise
const fetchUser = (id) => new Promise((resolve, reject) => {
  setTimeout(() => {
    if (id > 0) {
      resolve({ id, name: 'Alice' });
    } else {
      reject(new Error('Invalid user ID'));
    }
  }, 100);
});

// Consuming with .then()/.catch()
fetchUser(1)
  .then(user => console.log(user.name))
  .catch(err => console.error(err.message))
  .finally(() => console.log('Done'));  // Always runs

Promise Chaining

Each .then() returns a new Promise, enabling chains:

fetchUser(1)
  .then(user => fetchUserPosts(user.id))   // Returns another Promise
  .then(posts => renderPosts(posts))
  .then(result => console.log('Rendered:', result))
  .catch(err => {
    // Catches errors from ANY step in the chain
    console.error('Pipeline failed:', err);
  });

Common mistake: forgetting to return the inner Promise. Without return, the chain doesn't wait for the inner async operation.

async/await: Syntactic Sugar

async/await makes asynchronous code look synchronous. An async function always returns a Promise. await pauses execution until the Promise settles:

async function loadUserDashboard(userId) {
  const user = await fetchUser(userId);
  const posts = await fetchUserPosts(user.id);
  const comments = await fetchRecentComments(user.id);

  return { user, posts, comments };
}

// Call it
loadUserDashboard(1)
  .then(dashboard => render(dashboard))
  .catch(err => showError(err));

Error Handling Comparison

Error handling differs significantly between the two approaches:

// Promises: .catch() handles all errors in chain
function fetchDataPromise(url) {
  return fetch(url)
    .then(res => {
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      return res.json();
    })
    .catch(err => {
      console.error('Fetch failed:', err);
      return null;  // Return fallback
    });
}

// async/await: try/catch looks like synchronous error handling
async function fetchDataAsync(url) {
  try {
    const res = await fetch(url);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return await res.json();
  } catch (err) {
    console.error('Fetch failed:', err);
    return null;
  }
}

The async/await version is more readable for complex multi-step operations with conditional error handling.

Concurrency with Promise.all

The biggest performance trap with async/await: using await sequentially when operations are independent. Use Promise.all for parallel execution:

// SLOW: Sequential — total time = t1 + t2 + t3
async function loadDataSequential(userId) {
  const user = await fetchUser(userId);       // 200ms
  const posts = await fetchPosts(userId);     // 150ms
  const friends = await fetchFriends(userId); // 180ms
  return { user, posts, friends };            // Total: ~530ms
}

// FAST: Parallel — total time = max(t1, t2, t3)
async function loadDataParallel(userId) {
  const [user, posts, friends] = await Promise.all([
    fetchUser(userId),      // All three start simultaneously
    fetchPosts(userId),
    fetchFriends(userId),
  ]);
  return { user, posts, friends };  // Total: ~200ms
}

Promise Combinators

// Promise.all — fails fast if any rejects
const results = await Promise.all([p1, p2, p3]);

// Promise.allSettled — waits for all, never rejects
const results = await Promise.allSettled([p1, p2, p3]);
results.forEach(result => {
  if (result.status === 'fulfilled') console.log(result.value);
  else console.error(result.reason);
});

// Promise.race — settles with the first to finish
const fastest = await Promise.race([fetchFromRegion1(), fetchFromRegion2()]);

// Promise.any — resolves with first fulfilled (ignores rejections)
const firstSuccess = await Promise.any([mirror1(), mirror2(), mirror3()]);

When to Use Each

Scenario Promise async/await
Simple single async call OK Preferred
Multiple parallel requests Promise.all await Promise.all
Complex conditional flow Messy Preferred
Event handlers / streams Preferred Awkward

Common Pitfalls

// Pitfall 1: Unhandled rejection
fetchUser(1)  // No .catch() — silently fails in older Node.js

// Pitfall 2: await in forEach (doesn't work as expected)
[1, 2, 3].forEach(async (id) => {
  const user = await fetchUser(id);  // forEach doesn't await these
});
// Fix: use for...of or Promise.all
for (const id of [1, 2, 3]) {
  const user = await fetchUser(id);  // Sequential
}
await Promise.all([1, 2, 3].map(id => fetchUser(id)));  // Parallel

// Pitfall 3: Returning a Promise inside async (double-wrapping)
async function getUser() {
  return await fetchUser(1);  // Redundant await, but not harmful
  // return fetchUser(1);  // This also works fine
}

Frequently Asked Questions

Is async/await faster than Promises?

No — async/await is syntactic sugar over Promises. There's no performance difference. In fact, the V8 engine optimizes async functions heavily, so async/await can be marginally faster in hot paths due to fewer intermediate Promise allocations.

Can I use await at the top level?

Yes — with ES2022 Top-Level Await, you can use await directly in ES module files (.mjs) without wrapping in an async function. Widely supported in modern Node.js and all modern browsers.

How do I cancel a Promise?

Promises themselves are not cancellable. Use AbortController with fetch, or implement your own cancellation token pattern. Many libraries (axios, SWR) provide built-in cancellation support.

Build and test JavaScript functions with DevKits JSON Formatter to inspect async API responses. Use the DevKits Regex Tester for parsing patterns in async data processing pipelines.