Skip to content
JavaScript Promises Deep Dive — States, Chaining, and async/await Internals

JavaScript Promises Deep Dive — States, Chaining, and async/await Internals

DodaTech Updated Jun 15, 2026 8 min read

A Promise in JavaScript is an object representing the eventual completion or failure of an asynchronous operation. Think of it like a receipt from a food truck — you get the receipt immediately (the Promise), but the food arrives later (the resolved value). The Durga Antivirus Pro dashboard uses promises to coordinate multiple threat data sources, and Doda Browser uses them to manage parallel file downloads.

What You’ll Learn

  • The three Promise states and how transitions work
  • Promise chaining — passing values through .then()
  • Static methods — Promise.all, Promise.allSettled, Promise.race, Promise.any
  • How async/await works under the hood
  • The microtask queue and why promises are faster than setTimeout
  • Error handling patterns for promise chains
  • Parallel API calls with Promise.all

Why Promises Matter

Before promises, JavaScript used callbacks for everything asynchronous. Callbacks worked for simple cases but fell apart with complex workflows — leading to “callback hell.” Promises give you a flat, composable way to handle async operations. The async/await syntax builds on promises, making async code read like synchronous code.

Learning Path

    stateDiagram-v2
  [*] --> Pending
  Pending --> Fulfilled: resolve(value)
  Pending --> Rejected: reject(reason)
  Fulfilled --> [*]
  Rejected --> [*]
  
Prerequisites: You should understand JavaScript functions, callbacks, and basic Async Await concepts. This tutorial goes deep into how promises work internally.

Promise States

A Promise is always in one of three states:

  • Pending — Initial state. The operation hasn’t completed yet.
  • Fulfilled — The operation completed successfully. The .then() handlers run.
  • Rejected — The operation failed. The .catch() handlers run.

Once a promise settles (fulfills or rejects), its state is immutable — it can never change again:

const promise = new Promise((resolve, reject) => {
  resolve('First');
  reject('Second');   // Ignored — already resolved
  resolve('Third');   // Ignored — already resolved
});

promise.then(console.log); // Output: First

Anticipating confusion: A promise can’t go from fulfilled back to pending, or from rejected to fulfilled. Once settled, it stays settled forever. This makes promises predictable.

Promise Chaining

Every .then() returns a new Promise, making chaining possible:

fetch('/api/users/1')
  .then(response => {
    if (!response.ok) throw new Error('HTTP error');
    return response.json();
  })
  .then(user => {
    console.log(user.name);
    return fetch(`/api/posts?userId=${user.id}`);
  })
  .then(response => response.json())
  .then(posts => {
    console.log(`User has ${posts.length} posts`);
  })
  .catch(error => {
    console.error('Any error in the chain:', error);
  });

How the chain works:

  1. If .then() returns a value, the next .then() receives it.
  2. If .then() returns a Promise, the next .then() waits for it.
  3. If any .then() throws or returns a rejected Promise, execution jumps to the nearest .catch().

Value Transformations

Promise.resolve(5)
  .then(x => x * 2)          // Returns 10
  .then(x => Promise.resolve(x + 3))  // Returns Promise(13)
  .then(console.log);        // Output: 13

Error Recovery in Chains

You can recover from errors mid-chain:

fetch('/api/data')
  .then(response => {
    if (!response.ok) return { cached: true, data: getFromCache() };
    return response.json();
  })
  .then(data => {
    console.log('Using data:', data);
  });

Static Methods

Promise.all — Wait for All (Fail Fast)

const urls = ['/api/users', '/api/posts', '/api/comments'];
const promises = urls.map(url => fetch(url).then(r => r.json()));

try {
  const [users, posts, comments] = await Promise.all(promises);
  console.log(`Loaded ${users.length} users, ${posts.length} posts`);
} catch (error) {
  console.error('One request failed:', error);
}

Behavior: If any promise rejects, Promise.all immediately rejects with that error. Other promises continue running but their results are discarded.

Promise.allSettled — Wait for All (Never Rejects)

const results = await Promise.allSettled([
  fetch('/api/users').then(r => r.json()),
  fetch('/api/invalid').then(r => r.json()),
]);

results.forEach((result, i) => {
  if (result.status === 'fulfilled') {
    console.log(`Request ${i} succeeded:`, result.value);
  } else {
    console.log(`Request ${i} failed:`, result.reason);
  }
});

Output:

Request 0 succeeded: [...]
Request 1 failed: TypeError: Failed to fetch

Promise.race — First to Settle Wins

const timeout = new Promise((_, reject) =>
  setTimeout(() => reject(new Error('Timeout')), 5000)
);

const fetchWithTimeout = Promise.race([
  fetch('/api/data'),
  timeout
]);

Promise.any — First to Fulfill Wins

const servers = [
  fetch('https://backup1.example.com/data'),
  fetch('https://backup2.example.com/data'),
  fetch('https://primary.example.com/data'),
];

try {
  const fastest = await Promise.any(servers);
  console.log('Got data from fastest server');
} catch (error) {
  console.error('All servers failed:', error.errors);
}

Comparison table:

MethodResolvesRejects
Promise.allAll fulfillFirst rejection
Promise.allSettledAll settleNever
Promise.raceFirst settleFirst rejection
Promise.anyFirst fulfillmentAll reject

Async/Await Internals

async/await is syntactic sugar over promises. The JavaScript engine transforms async functions into state machines — like generators that yield control:

// This async function:
async function loadUser(id) {
  const response = await fetch(`/api/users/${id}`);
  const user = await response.json();
  return user;
}

// Is essentially this (simplified):
function loadUser(id) {
  return fetch(`/api/users/${id}`)
    .then(response => response.json());
}

Key insight: Every await creates a “pause point” where the function yields control back to the event loop. While the promise is pending, other code can run:

async function example() {
  console.log('A');
  await Promise.resolve();  // Yields to microtask queue
  console.log('B');
}
example();
console.log('C');
// Output: A, C, B

The Microtask Queue

Promise callbacks (.then(), .catch(), .finally() and code after await) go into the microtask queue, which has priority over the macrotask queue (setTimeout, setInterval, I/O):

console.log('1: Start');

setTimeout(() => console.log('2: Timeout'), 0);

Promise.resolve().then(() => console.log('3: Promise'));

console.log('4: End');

// Output:
// 1: Start
// 4: End
// 3: Promise  ← microtask runs before macrotask
// 2: Timeout

Why this matters: Promises are always processed before setTimeout callbacks, even when the timeout is 0ms. This means promise-based code often runs faster than callback-based alternatives.

Error Handling in Promise Chains

async function fetchUserData(userId) {
  try {
    const response = await fetch(`/api/users/${userId}`);

    if (response.status === 404) {
      throw new NotFoundError('User not found');
    }
    if (!response.ok) {
      throw new ApiError(`HTTP ${response.status}`);
    }

    return await response.json();

  } catch (error) {
    if (error instanceof NotFoundError) {
      return null; // Graceful degradation
    }
    // Log and re-throw unexpected errors
    console.error('Unexpected error:', error);
    throw error;
  }
}

Best practice: Handle specific error types with instanceof checks. Let unexpected errors propagate to a global handler.

Common Mistakes

1. Not returning a promise from .then()

fetch('/api/data')
  .then(response => {
    response.json(); // Missing return!
  })
  .then(data => {
    console.log(data); // undefined!
  });

Fix: Always return inside .then() if you want the next handler to receive the value.

2. Sequential promises that should be parallel

const a = await fetch('/api/a');
const b = await fetch('/api/b'); // Waits for 'a' to finish

Fix: Use Promise.all for independent requests.

3. Forgetting to handle promise rejections

function loadData() {
  return fetch('/api/data').then(r => r.json());
  // If fetch fails, promise rejects — unhandled!
}
loadData();

Fix: Always add .catch() or handle with try/catch in the caller.

4. Creating promise chains inside loops

for (const url of urls) {
  const data = await fetch(url); // Sequential!
}

Fix: Create promises first, then use Promise.all.

5. Nesting promises instead of chaining

fetch('/api/a').then(dataA => {
  fetch('/api/b').then(dataB => {
    // Nesting recreates callback hell
  });
});

Fix: Return inner promises and chain flatly.

Practice Questions

  1. What happens if you call resolve() twice on the same promise? Only the first call has an effect. Once a promise settles, it’s immutable.

  2. What’s the difference between Promise.all and Promise.allSettled? Promise.all rejects fast (if any promise rejects). Promise.allSettled waits for all to complete and returns results for each, never rejecting.

  3. Why does Promise code run before setTimeout(0)? Promise .then() callbacks go to the microtask queue, which is processed before the macrotask queue where setTimeout callbacks go.

  4. How does async/await relate to promises? async/await is syntactic sugar over promises. An async function always returns a promise, and await unwraps the promise value.

Challenge: Implement a promisePool(tasks, concurrency) function that runs async tasks with a maximum concurrency limit, returning all results.

Mini Project: Parallel API Data Loader

async function loadAllData(userIds) {
  const results = await Promise.allSettled(
    userIds.map(id =>
      fetch(`https://api.example.com/users/${id}`)
        .then(r => {
          if (!r.ok) throw new Error(`HTTP ${r.status}`);
          return r.json();
        })
    )
  );

  const users = [];
  const errors = [];

  results.forEach((result, index) => {
    if (result.status === 'fulfilled') {
      users.push(result.value);
    } else {
      errors.push({ userId: userIds[index], error: result.reason });
    }
  });

  return { users, errors };
}

const { users, errors } = await loadAllData([1, 2, 3]);
console.log(`Loaded ${users.length} users, ${errors.length} errors`);

FAQ

What is the difference between resolved and fulfilled?
A “resolved” promise is settled and can’t change state. “Fulfilled” means it resolved successfully. All fulfilled promises are resolved, but not all resolved promises are fulfilled (some are rejected).
Can I cancel a promise?
Not natively. Use AbortController with fetch, or third-party libraries like Bluebird for cancellable promises.
What happens to errors after .catch()?
If .catch() doesn’t throw or return a rejected promise, the chain recovers and subsequent .then() handlers run normally.
What is the event loop?
JavaScript’s mechanism for handling async operations. It processes synchronous code first, then microtasks (promises), then macrotasks (timers, I/O).
When should I use .finally()?
For cleanup that must run whether the promise fulfills or rejects — hiding spinners, releasing resources, closing connections.

What’s Next

LessonDescription
JavaScript HomeBack to the JavaScript hub
https://tutorials.dodatech.com/programming-languages/javascript/js-async/Async JavaScript fundamentals
https://tutorials.dodatech.com/programming-languages/javascript/js-error-handling/Error handling patterns
https://tutorials.dodatech.com/programming-languages/javascript/js-fetch-api/Fetch API — HTTP requests
Node.jsAsync patterns in Node.js

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

What’s Next

Congratulations on completing this JavaScript Promises Deep Dive tutorial! Here’s where to go from here:

  • Practice daily — Consistency is more important than long study sessions
  • Build a project — Apply what you learned by building something real
  • Explore related topics — Check out other tutorials in the same category
  • Join the community — Discuss with other learners and share your progress

Remember: every expert was once a beginner. Keep coding!

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro