HeoLab
ToolsBlogAboutContact
HeoLab

Free developer tools with AI enhancement. Built for developers who ship.

Tools

  • JSON Formatter
  • JWT Decoder
  • Base64 Encoder
  • Timestamp Converter
  • Regex Tester
  • All Tools →

Resources

  • Blog
  • What is JSON?
  • JWT Deep Dive
  • Base64 Explained

Company

  • About
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 HeoLab. All rights reserved.

Tools work in your browser. Zero data retention.

HomeBlogAsync/Await vs Promises in JavaScript: A Complete Guide
Table of Contents▾
  • The Event Loop (30 Seconds)
  • Promises
  • Async/Await
  • Parallel Execution
  • Error Handling Patterns
  • Common Mistakes
  • Async Iteration (ES2018)
tutorials#javascript#async#promises

Async/Await vs Promises in JavaScript: A Complete Guide

Master asynchronous JavaScript — understand the event loop, Promises, async/await, error handling, parallel execution, and common pitfalls.

Trong Ngo
February 23, 2026
4 min read

Asynchronous programming is JavaScript's superpower — and its biggest source of bugs. This guide takes you from callbacks through Promises to async/await, with the patterns that actually matter in production.

The Event Loop (30 Seconds)

JavaScript is single-threaded. The event loop lets it handle async operations without blocking:

Call Stack     Microtask Queue    Task Queue (macrotask)
----------     ---------------    ---------------------
main()         Promise callbacks   setTimeout callbacks
fetchData()    .then() handlers    setInterval callbacks
               async/await         I/O callbacks

Order: Call Stack → Microtasks (ALL of them) → One Task → Microtasks → ...

Promises and async/await run in the microtask queue — they have priority over setTimeout callbacks.

Promises

// Creating a Promise
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));

const fetchUser = (id) => new Promise((resolve, reject) => {
  fetch(`/api/users/${id}`)
    .then(r => r.ok ? resolve(r.json()) : reject(new Error('Not found')))
    .catch(reject);
});

// Consuming a Promise
fetchUser(1)
  .then(user => console.log(user))
  .catch(err => console.error(err))
  .finally(() => console.log('always runs'));

// Chaining
fetchUser(1)
  .then(user => fetchOrders(user.id))  // returns another Promise
  .then(orders => display(orders))
  .catch(err => handleError(err));      // catches any error in the chain

Async/Await

Async/await is syntactic sugar over Promises — it makes async code look synchronous:

// Equivalent to the .then() chain above
async function loadUserOrders(userId) {
  try {
    const user = await fetchUser(userId);
    const orders = await fetchOrders(user.id);
    display(orders);
  } catch (err) {
    handleError(err);
  } finally {
    setLoading(false);
  }
}

// await can only be used inside async functions
// (or at the top level of ES modules)

// Arrow function syntax
const loadUser = async (id) => {
  const user = await fetchUser(id);
  return user;
};

Parallel Execution

The most common performance mistake: awaiting things sequentially that could run in parallel.

// ✗ Sequential — 2000ms total
const user = await fetchUser(id);     // 1000ms
const posts = await fetchPosts(id);   // 1000ms

// ✓ Parallel — 1000ms total
const [user, posts] = await Promise.all([
  fetchUser(id),
  fetchPosts(id),
]);

// Promise.allSettled — continues even if some fail
const results = await Promise.allSettled([fetchA(), fetchB(), fetchC()]);
const successes = results
  .filter(r => r.status === 'fulfilled')
  .map(r => r.value);

// Promise.race — first one wins
const result = await Promise.race([
  fetch('/api/data'),
  new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 5000)),
]);

// Promise.any — first successful one wins (ES2021)
const fastest = await Promise.any([fetchFromCDN1(), fetchFromCDN2()]);

Error Handling Patterns

// Pattern 1: try/catch (best for complex flows)
async function getData() {
  try {
    const data = await riskyOperation();
    return data;
  } catch (err) {
    if (err instanceof NetworkError) return null;  // recoverable
    throw err;  // rethrow unrecoverable errors
  }
}

// Pattern 2: Go-style [error, result] (avoids nested try/catch)
async function safeAwait(promise) {
  try {
    return [null, await promise];
  } catch (err) {
    return [err, null];
  }
}

const [err, user] = await safeAwait(fetchUser(id));
if (err) return handleError(err);

// Pattern 3: .catch() on the promise (for optional errors)
const user = await fetchUser(id).catch(() => null);  // null on error
if (!user) return notFound();

Common Mistakes

// ✗ Mistake 1: Forgetting await (promise goes unhandled)
async function bad() {
  const data = fetchData();  // missing await — data is a Promise!
  console.log(data.value);   // undefined
}

// ✗ Mistake 2: await inside forEach (doesn't work!)
items.forEach(async item => {
  await process(item);  // forEach doesn't await these
});

// ✓ Use for...of for sequential async
for (const item of items) {
  await process(item);
}

// ✓ Use Promise.all for parallel async
await Promise.all(items.map(item => process(item)));

// ✗ Mistake 3: Creating unnecessary Promise wrappers
async function bad() {
  return new Promise(resolve => {  // redundant!
    resolve(fetch('/api'));
  });
}

// ✓ Just return the promise directly
async function good() {
  return fetch('/api');
}

// ✗ Mistake 4: Not catching unhandled rejections
// In Node.js, unhandled promise rejections crash the process (Node 15+)
// Always handle errors or attach .catch()

Async Iteration (ES2018)

// Async generators — for streaming data
async function* streamData(url) {
  const response = await fetch(url);
  const reader = response.body.getReader();

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    yield new TextDecoder().decode(value);
  }
}

// Consume with for-await-of
for await (const chunk of streamData('/api/stream')) {
  process.stdout.write(chunk);
}

Related Articles

JavaScript Array Methods: The Complete Guide with Examples

5 min read

TypeScript Best Practices Every Developer Should Follow in 2025

4 min read

UUID Guide for Developers: v1, v4, v5, and ULID Explained

4 min read

Back to Blog

Table of Contents

  • The Event Loop (30 Seconds)
  • Promises
  • Async/Await
  • Parallel Execution
  • Error Handling Patterns
  • Common Mistakes
  • Async Iteration (ES2018)

Related Articles

JavaScript Array Methods: The Complete Guide with Examples

5 min read

TypeScript Best Practices Every Developer Should Follow in 2025

4 min read

UUID Guide for Developers: v1, v4, v5, and ULID Explained

4 min read