JavaScript Promise & async/await Guide: Master Asynchronous Programming

Published April 2026 · JavaScript Tutorial · Try JSON Formatter →

Asynchronous programming is fundamental to JavaScript. Everything from fetching data over the network to reading files to setting timers relies on async operations. Promises provide a clean, standardized way to handle async work, and the async/await syntax makes Promise-based code read like synchronous code. Together, they form the backbone of modern JavaScript development.

This guide covers Promises from first principles through to advanced patterns used in production applications.

Understanding the Problem: Why Promises?

Before Promises, async code used callbacks — functions passed as arguments to be called later. Callbacks led to deeply nested code (callback hell), error handling was inconsistent, and composing multiple async operations was messy.

// Callback hell (the old way)
getUser(id, (user) => {
  getOrders(user.id, (orders) => {
    getOrderDetails(orders[0].id, (details) => {
      getShipping(details.addressId, (shipping) => {
        // Five levels deep...
      });
    });
  });
});

Promises flatten this into a clean, chainable structure.

Creating Promises

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

const myPromise = new Promise((resolve, reject) => {
  const success = true;

  if (success) {
    resolve('Operation succeeded!');  // Value on success
  } else {
    reject(new Error('Operation failed'));  // Error on failure
  }
});

The executor function runs immediately when you create the Promise. Call resolve(value) on success and reject(error) on failure.

Promisifying Callback-Based APIs

Wrap older callback-based functions in Promises:

function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

function readFilePromise(path) {
  return new Promise((resolve, reject) => {
    fs.readFile(path, 'utf8', (err, data) => {
      if (err) reject(err);
      else resolve(data);
    });
  });
}

Consuming Promises with .then() and .catch()

myPromise
  .then(result => {
    console.log(result); // 'Operation succeeded!'
    return 'Next step';
  })
  .then(nextResult => {
    console.log(nextResult); // 'Next step'
  })
  .catch(error => {
    console.error('Caught:', error.message);
  })
  .finally(() => {
    console.log('This runs no matter what');
  });

Each .then() returns a new Promise, allowing you to chain operations sequentially. If any Promise in the chain rejects, execution skips to the nearest .catch(). The .finally() handler runs regardless of outcome — useful for cleanup.

async/await: The Modern Syntax

The async/await syntax makes Promise code look synchronous. An async function always returns a Promise, and await pauses execution until the Promise resolves.

async function fetchUserData(userId) {
  try {
    const user = await getUser(userId);
    const orders = await getOrders(user.id);
    const details = await getOrderDetails(orders[0].id);

    return { user, orders, details };
  } catch (error) {
    console.error('Failed to fetch user data:', error);
    throw error;
  }
}

// Usage
fetchUserData(123)
  .then(data => console.log(data))
  .catch(err => console.error(err));

Compare this to the callback hell example above — the same logic is now flat, readable, and uses standard try/catch for error handling.

Top-Level await

In ES Modules, you can use await at the top level without wrapping it in an async function:

// app.js (type="module")
const config = await fetch('/config.json').then(r => r.json());
console.log('App config loaded:', config);

Sequential vs Parallel Execution

One of the most important concepts in async programming is knowing when to run operations sequentially vs in parallel.

Sequential (One After Another)

// Each await waits for the previous one to finish
async function sequential() {
  const user = await fetchUser(1);      // 1 second
  const posts = await fetchPosts(user.id); // 1 second
  const comments = await fetchComments(posts[0].id); // 1 second
  // Total: ~3 seconds
}

Parallel (All at Once)

// All fetches start simultaneously
async function parallel() {
  const [user, posts, comments] = await Promise.all([
    fetchUser(1),           // 1 second
    fetchPosts(1),          // 1 second
    fetchComments(1),       // 1 second
  ]);
  // Total: ~1 second (all run concurrently)
}

Use Promise.all() when you need to wait for multiple independent async operations. It runs them all in parallel and resolves when every one has completed. If any single Promise rejects, the entire Promise.all() rejects.

Promise Static Methods

Promise.all() — All Must Succeed

const results = await Promise.all([
  fetch('/api/users'),
  fetch('/api/posts'),
  fetch('/api/comments'),
]);

const [users, posts, comments] = results.map(r => r.json());

Promise.allSettled() — Wait for All, Regardless of Failures

const results = await Promise.allSettled([
  fetch('/api/users'),
  fetch('/api/posts'),  // This one might fail
  fetch('/api/comments'),
]);

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

Use Promise.allSettled() when you need results from all operations even if some fail. Each result has a status of either 'fulfilled' or 'rejected'.

Promise.race() — First to Settle Wins

const result = await Promise.race([
  fetch('/api/fast-endpoint'),
  fetch('/api/slow-endpoint'),
  delay(3000).then(() => { throw new Error('Timeout'); }),
]);

Promise.race() resolves or rejects with the value of the first Promise to settle. This is useful for implementing timeouts.

Promise.any() — First to Succeed

const result = await Promise.any([
  fetch('https://primary-api.com/data'),
  fetch('https://backup-api.com/data'),
  fetch('https://fallback-api.com/data'),
]);

Promise.any() resolves with the first fulfilled Promise. It only rejects if ALL Promises reject (with an AggregateError). This is ideal for trying multiple sources and using whichever responds first.

Error Handling Patterns

try/catch with async/await

async function safeFetch(url) {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    return await response.json();
  } catch (error) {
    if (error instanceof TypeError) {
      throw new Error('Network error — check your connection');
    }
    throw error;
  }
}

Error Boundary Wrapper

async function withErrorHandling(fn, fallbackValue = null) {
  try {
    return await fn();
  } catch (error) {
    console.error(`Error in ${fn.name}:`, error);
    return fallbackValue;
  }
}

// Usage
const users = await withErrorHandling(() => fetchUsers(), []);
const posts = await withErrorHandling(() => fetchPosts(), []);
// Even if both fail, your app continues with empty arrays

Common Anti-Patterns to Avoid

Unnecessary await in return

// Unnecessary — the async function already wraps in a Promise
async function getUser() {
  return await fetchUser(1);
}

// Better — let the caller await
async function getUser() {
  return fetchUser(1);
}

Forgotten await

// BUG: data is a Promise, not the actual data
async function processData() {
  const data = fetchData(); // Missing await!
  console.log(data); // Promise { <pending> }
}

// Fixed
async function processData() {
  const data = await fetchData();
  console.log(data); // Actual data
}

Sequential when parallel would work

// Slow: 3 seconds total
const a = await taskA(); // 1s
const b = await taskB(); // 1s
const c = await taskC(); // 1s

// Fast: 1 second total (parallel)
const [a, b, c] = await Promise.all([taskA(), taskB(), taskC()]);

Utility Functions

// Retry a failing operation
async function retry(fn, maxRetries = 3, delayMs = 1000) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fn();
    } catch (error) {
      if (i === maxRetries - 1) throw error;
      await new Promise(r => setTimeout(r, delayMs * (i + 1)));
    }
  }
}

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

// Usage
const data = await retry(() => fetchWithTimeout('/api/data', 5000), 3);

Frequently Asked Questions

What is the difference between Promise and async/await?

async/await is syntactic sugar built on top of Promises. async functions return Promises, and await pauses execution until a Promise resolves. They are equivalent — async/await just makes the code look synchronous and easier to read.

What happens if I forget to await a Promise?

The Promise starts executing but its result is ignored. You get a pending Promise object instead of the resolved value. Always await Promises unless you intentionally want fire-and-forget behavior.

How do I run multiple Promises in parallel?

Use Promise.all() to run multiple Promises simultaneously and wait for all to resolve. Use Promise.allSettled() if you want results regardless of individual failures.

Can I use try/catch with async/await?

Yes, that is the primary advantage. Wrap await calls in try/catch blocks to handle rejected Promises the same way you handle synchronous errors.

What is Promise.allSettled and when should I use it?

Promise.allSettled() waits for ALL Promises to complete (resolve or reject) and returns an array of results with a status field. Use it when you need results from all operations even if some fail.