Toolmingo
Guides7 min read

JavaScript Promises and Async/Await: Complete Guide

Master JavaScript Promises and async/await with clear examples. Covers Promise chaining, error handling, Promise.all, race, allSettled, and common async patterns.

Promises and async/await are the backbone of modern JavaScript. Once you understand how the event loop, Promise states, and await interact, async code becomes as readable as synchronous code. This guide covers everything from basics to production patterns.


Quick-reference table

Syntax Purpose Returns
new Promise((resolve, reject) => {}) Create a promise Promise<T>
promise.then(fn) Handle resolved value Promise<T>
promise.catch(fn) Handle rejection Promise<T>
promise.finally(fn) Run regardless of outcome Promise<T>
await promise Wait for settlement (async fn only) T or throws
Promise.resolve(value) Create pre-resolved promise Promise<T>
Promise.reject(reason) Create pre-rejected promise Promise<never>
Promise.all([...]) Wait for all; fail fast Promise<T[]>
Promise.allSettled([...]) Wait for all; never rejects Promise<Result[]>
Promise.race([...]) First to settle wins Promise<T>
Promise.any([...]) First to resolve wins Promise<T>

Promise states

A Promise is always in one of three states:

Pending → Fulfilled (resolved with a value)
Pending → Rejected  (rejected with a reason)

Once settled (fulfilled or rejected), a Promise never changes state. This is the key invariant.

const p = new Promise((resolve, reject) => {
  // Async work here
  setTimeout(() => resolve("done"), 1000);
});

console.log(p); // Promise { <pending> }

p.then((value) => console.log(value)); // "done" (after 1s)

Creating promises

// Wrapping a callback-based API
function readFile(path) {
  return new Promise((resolve, reject) => {
    fs.readFile(path, "utf8", (err, data) => {
      if (err) reject(err);
      else resolve(data);
    });
  });
}

// Already-settled promises (useful in tests)
Promise.resolve(42).then(console.log);   // 42
Promise.reject(new Error("oops")).catch(console.error); // Error: oops

.then() chaining

Each .then() returns a new Promise with the return value of the callback:

fetch("https://api.example.com/user/1")
  .then((res) => res.json())          // returns Promise<data>
  .then((data) => data.name)          // returns Promise<string>
  .then((name) => console.log(name))  // "Alice"
  .catch((err) => console.error(err));

Rule: If you return a value from .then(), the next .then() gets that value. If you return a Promise, the chain waits for it.

// Returning a Promise flattens the chain (no nested Promise<Promise<T>>)
Promise.resolve(1)
  .then((n) => Promise.resolve(n + 1)) // returns Promise<2>, not Promise<Promise<2>>
  .then((n) => console.log(n));         // 2

async / await

async functions always return a Promise. await pauses execution inside the async function until the Promise settles.

async function fetchUser(id) {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  const data = await res.json();
  return data;
}

// Calling an async function
fetchUser(1).then(console.log).catch(console.error);

// Or inside another async function
async function main() {
  const user = await fetchUser(1);
  console.log(user.name);
}

Error handling with try/catch

async function loadData() {
  try {
    const res = await fetch("/api/data");
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return await res.json();
  } catch (err) {
    console.error("Failed:", err.message);
    return null; // graceful fallback
  }
}

Sequential vs parallel execution

Sequential (one after another)

async function sequential() {
  const a = await fetchA(); // waits for A
  const b = await fetchB(); // then waits for B
  // Total time: time(A) + time(B)
  return [a, b];
}

Parallel (both at once)

async function parallel() {
  const [a, b] = await Promise.all([fetchA(), fetchB()]);
  // Total time: max(time(A), time(B))
  return [a, b];
}

Start both, then await both — don't await inside the array literal:

// ✅ Correct: both start immediately
const pA = fetchA();
const pB = fetchB();
const [a, b] = await Promise.all([pA, pB]);

// ❌ Wrong: sequential despite Promise.all
const [a, b] = await Promise.all([await fetchA(), await fetchB()]);

Promise combinators

Promise.all — all or nothing

Resolves with all values. Rejects immediately if any rejects.

const results = await Promise.all([
  fetch("/api/users"),
  fetch("/api/posts"),
  fetch("/api/comments"),
]);
// If any fetch fails, the whole thing rejects

Promise.allSettled — wait for all, never rejects

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

for (const result of results) {
  if (result.status === "fulfilled") {
    console.log("OK:", result.value);
  } else {
    console.error("Failed:", result.reason);
  }
}

Use allSettled when you want partial results even if some fail.

Promise.race — first to settle wins

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

const data = await withTimeout(fetchData(), 5000);

Promise.any — first to resolve wins

Rejects only if all promises reject (AggregateError).

// Try multiple CDNs, use whichever responds first
const asset = await Promise.any([
  fetch("https://cdn1.example.com/lib.js"),
  fetch("https://cdn2.example.com/lib.js"),
  fetch("https://cdn3.example.com/lib.js"),
]);

Practical patterns

Retry with backoff

async function retry(fn, retries = 3, delay = 500) {
  try {
    return await fn();
  } catch (err) {
    if (retries === 0) throw err;
    await new Promise((r) => setTimeout(r, delay));
    return retry(fn, retries - 1, delay * 2);
  }
}

const data = await retry(() => fetch("/api/flaky").then((r) => r.json()));

Concurrent with limit

async function batchProcess(items, fn, concurrency = 3) {
  const results = [];
  for (let i = 0; i < items.length; i += concurrency) {
    const batch = items.slice(i, i + concurrency);
    const batchResults = await Promise.all(batch.map(fn));
    results.push(...batchResults);
  }
  return results;
}

const processed = await batchProcess(urls, fetchAndProcess);

Async initialization (singleton)

class DbClient {
  static #instance = null;
  static #initPromise = null;

  static async getInstance() {
    if (!DbClient.#instance) {
      if (!DbClient.#initPromise) {
        DbClient.#initPromise = DbClient.#init();
      }
      DbClient.#instance = await DbClient.#initPromise;
    }
    return DbClient.#instance;
  }

  static async #init() {
    // expensive setup
    return new DbClient();
  }
}

async in forEach — it doesn't work as expected

// ❌ forEach doesn't await — all fire simultaneously, forEach returns before they finish
items.forEach(async (item) => {
  await processItem(item);
});

// ✅ Use for...of for sequential
for (const item of items) {
  await processItem(item);
}

// ✅ Use Promise.all for parallel
await Promise.all(items.map((item) => processItem(item)));

6 common mistakes

1. Forgetting to await

// ❌ data is a Promise, not the value
const data = fetchData();
console.log(data.name); // undefined

// ✅
const data = await fetchData();
console.log(data.name);

2. Unhandled rejections

// ❌ If fetchData() rejects, you get UnhandledPromiseRejection
fetchData().then(process);

// ✅ Always attach .catch() or use try/catch with await
fetchData().then(process).catch(handleError);

3. Promise constructor anti-pattern

// ❌ Wrapping an already-async function in new Promise
async function bad() {
  return new Promise(async (resolve, reject) => {
    try {
      const data = await fetch("/api");
      resolve(data);
    } catch (e) {
      reject(e);
    }
  });
}

// ✅ Just use async/await directly
async function good() {
  return fetch("/api"); // async functions auto-wrap in Promise
}

4. Missing await inside try/catch

// ❌ catch doesn't catch the rejection (promise created outside try)
async function bad() {
  try {
    const p = fetchData(); // starts the promise
    doSomethingElse();
    return await p;       // if this throws, DOES get caught
  } catch (e) { /* fine */ }
}

// Actually the real trap:
async function trap() {
  try {
    return fetchData(); // ❌ no await — rejection escapes the try/catch
  } catch (e) { /* never runs */ }
}

5. Sequential instead of parallel

// ❌ 3 seconds total (1s + 1s + 1s)
const a = await delay(1000);
const b = await delay(1000);
const c = await delay(1000);

// ✅ 1 second total
const [a, b, c] = await Promise.all([delay(1000), delay(1000), delay(1000)]);

6. Promise.all fail-fast in production

// ❌ One failure kills everything
const results = await Promise.all([criticalOp(), optionalOp()]);

// ✅ Use allSettled if some operations are optional
const [critical, optional] = await Promise.allSettled([criticalOp(), optionalOp()]);
if (critical.status === "rejected") throw critical.reason;
const optionalData = optional.status === "fulfilled" ? optional.value : null;

6 FAQ

Q: When should I use .then() vs async/await?
Both are equivalent. Prefer async/await for readability — it reads like synchronous code and makes error handling with try/catch straightforward. Use .then() when chaining short transformations or in non-async contexts.

Q: Is await blocking?
No. await pauses only the current async function — it yields control back to the event loop. Other code (other promises, event handlers, timers) can run while your function is "waiting."

Q: Can I use await at the top level?
Yes — in ES modules (.mjs files or "type": "module" in package.json). In CommonJS (require), you must wrap in an async function.

Q: What's the difference between Promise.all and Promise.allSettled?
Promise.all rejects immediately if any promise rejects (fail-fast). Promise.allSettled always resolves with an array of {status, value | reason} objects — useful when you want partial results.

Q: How do I convert a callback-based API to a Promise?
Wrap it in new Promise((resolve, reject) => { ... }). Node.js also provides util.promisify() for standard (err, result) callbacks:

const { promisify } = require("util");
const readFile = promisify(fs.readFile);
const content = await readFile("file.txt", "utf8");

Q: Why does async in map/forEach not work as expected?
Array.map() returns an array of Promises, not resolved values. Use Promise.all(arr.map(async fn)) to wait for all. forEach ignores the returned Promises entirely — use for...of for sequential async iteration.


Understanding Promises and async/await unlocks all modern JavaScript APIs: fetch, Web Crypto, IndexedDB, Service Workers, and virtually every Node.js library. Start with the quick-reference table above, then practice the parallel vs sequential pattern — that distinction alone eliminates most async bugs.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools