Home Module 14 Promises

1. Introduction

A Promise is an object representing the eventual completion (or failure) of an asynchronous operation. It is a placeholder for a value that isn't available yet. Promises were introduced to solve callback hell — sequential async operations can be chained with .then() instead of nested.

Every modern async API returns Promises: fetch, navigator.clipboard.writeText, import(), and most browser APIs. You must understand Promises to use them.

2. Theory

2.1 Promise states

// A Promise is always in one of three states:
// pending   — initial state, operation not yet complete
// fulfilled — operation succeeded, value available
// rejected  — operation failed, reason available

// Once fulfilled or rejected, a Promise is "settled" — state never changes again

const p = new Promise((resolve, reject) => {
  // Async operation here...
  const success = true;

  if (success) {
    resolve('Data loaded!'); // → fulfilled
  } else {
    reject(new Error('Load failed')); // → rejected
  }
});

console.log(p); // Promise { <pending> } — not yet settled

2.2 Creating Promises

// Wrap a callback-based function in a Promise
function delay(ms) {
  return new Promise(resolve => {
    setTimeout(resolve, ms); // resolve() after ms
  });
}

// Promise that may fail
function fetchUser(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const users = { 1: 'Alice', 2: 'Bob' };
      const user = users[id];
      if (user) {
        resolve({ id, name: user }); // success
      } else {
        reject(new Error(`User ${id} not found`)); // failure
      }
    }, 100);
  });
}

// Already-settled Promises — useful for testing and defaults
const immediate = Promise.resolve(42);     // fulfilled immediately
const failed    = Promise.reject(new Error('Oops')); // rejected immediately

2.3 Consuming Promises — then, catch, finally

fetchUser(1)
  .then(user => {
    console.log('Found:', user.name); // 'Found: Alice'
    return user; // pass to next .then
  })
  .then(user => {
    console.log('Processing:', user.id);
  })
  .catch(err => {
    console.error('Error:', err.message); // runs on rejection
  })
  .finally(() => {
    console.log('Done!'); // always runs — success or failure
  });

// .catch is shorthand for .then(undefined, onRejected)
// .finally doesn't receive a value — it's for cleanup

2.4 Promise chaining — replacing callback hell

// Callback version (nested)
login(user, pwd, (err, user) => {
  getProfile(user.id, (err, profile) => {
    getPosts(profile.id, (err, posts) => { ... });
  });
});

// Promise version (flat chain)
login(user, pwd)
  .then(user     => getProfile(user.id))
  .then(profile  => getPosts(profile.id))
  .then(posts    => { console.log(posts); })
  .catch(err     => console.error(err)); // ONE catch handles ALL errors

2.5 Promise.all — parallel, all must succeed

// Waits for ALL promises to resolve. Rejects if ANY rejects.
const p1 = fetchUser(1);
const p2 = fetchUser(2);
const p3 = delay(200).then(() => 'timer done');

Promise.all([p1, p2, p3])
  .then(([user1, user2, timerMsg]) => {
    console.log(user1.name); // 'Alice'
    console.log(user2.name); // 'Bob'
    console.log(timerMsg);   // 'timer done'
  })
  .catch(err => console.error('At least one failed:', err));

// Use when: you need ALL results before proceeding
// (load user + permissions + settings before rendering dashboard)

2.6 Promise.allSettled — parallel, all complete regardless

// Waits for ALL promises to settle (resolve OR reject)
const requests = [fetchUser(1), fetchUser(99), fetchUser(2)];

Promise.allSettled(requests)
  .then(results => {
    results.forEach(result => {
      if (result.status === 'fulfilled') {
        console.log('OK:', result.value.name);
      } else {
        console.error('Failed:', result.reason.message);
      }
    });
  });
// Use when: you want all results even if some fail
// (batch image upload — show which succeeded, which failed)

2.7 Promise.race — first to settle wins

// Resolves/rejects with the first settled promise
const timeout = new Promise((_, reject) =>
  setTimeout(() => reject(new Error('Request timed out')), 5000)
);

const dataFetch = fetch('https://api.example.com/data').then(r => r.json());

// Whichever settles first — the fetch result OR the timeout error
Promise.race([dataFetch, timeout])
  .then(data => console.log('Data:', data))
  .catch(err => console.error(err.message)); // "Request timed out" if slow

2.8 Promise.any — first to succeed

// Resolves with the first FULFILLED promise
// Rejects only if ALL reject (AggregateError)
const mirrors = [
  fetch('https://mirror1.example.com/data'),
  fetch('https://mirror2.example.com/data'),
  fetch('https://mirror3.example.com/data')
];

Promise.any(mirrors)
  .then(response => response.json())
  .then(data => console.log('Fastest mirror:', data))
  .catch(err => console.error('All mirrors failed'));

2.9 Common patterns

// Retry pattern
function retryOnce(fn) {
  return fn().catch(() => fn()); // try again on failure
}

// Timeout wrapper
function withTimeout(promise, ms) {
  const timer = new Promise((_, reject) =>
    setTimeout(() => reject(new Error('Timed out')), ms)
  );
  return Promise.race([promise, timer]);
}

// Sequential from array
const ids = [1, 2, 3, 4, 5];
ids.reduce((chain, id) =>
  chain.then(() => fetchUser(id)).then(u => console.log(u.name)),
  Promise.resolve()
);

3. Real World Example

// Dashboard loader — parallel fetches, one error handler
function loadDashboard(userId) {
  const startEl  = document.querySelector('#status');
  startEl.textContent = 'Loading...';

  return Promise.all([
    fetch(`/api/users/${userId}`).then(r => r.json()),
    fetch(`/api/users/${userId}/stats`).then(r => r.json()),
    fetch(`/api/notifications`).then(r => r.json())
  ])
  .then(([user, stats, notifications]) => {
    renderUser(user);
    renderStats(stats);
    renderNotifications(notifications);
    startEl.textContent = `Welcome, ${user.name}!`;
    return { user, stats, notifications };
  })
  .catch(err => {
    startEl.textContent = 'Failed to load dashboard.';
    console.error(err);
    throw err; // re-throw so callers can handle further
  })
  .finally(() => {
    document.querySelector('#spinner').hidden = true;
  });
}

4. Code Example

<button id="fetch-all">Fetch All (Parallel)</button>
<button id="fetch-seq">Fetch Sequence</button>
<div id="results"></div>

<script>
  // Simulated promise-based API
  function simulateFetch(name, ms, fail = false) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        fail
          ? reject(new Error(`${name} failed`))
          : resolve(`${name} data (took ${ms}ms)`);
      }, ms);
    });
  }

  const results = document.querySelector('#results');

  function log(msg, color = 'inherit') {
    const p = document.createElement('p');
    p.textContent = msg;
    p.style.color = color;
    results.appendChild(p);
  }

  document.querySelector('#fetch-all').addEventListener('click', () => {
    results.innerHTML = '';
    log('Fetching all in parallel...');
    const start = Date.now();

    Promise.all([
      simulateFetch('Users',    400),
      simulateFetch('Products', 600),
      simulateFetch('Orders',   300)
    ])
    .then(([users, products, orders]) => {
      log(users,    'green');
      log(products, 'blue');
      log(orders,   'purple');
      log(`Done in ${Date.now() - start}ms`);
    })
    .catch(err => log('Error: ' + err.message, 'red'));
  });

  document.querySelector('#fetch-seq').addEventListener('click', () => {
    results.innerHTML = '';
    log('Fetching sequentially...');
    const start = Date.now();

    simulateFetch('Users', 400)
      .then(users => {
        log(users, 'green');
        return simulateFetch('Profile', 300);
      })
      .then(profile => {
        log(profile, 'blue');
        return simulateFetch('Posts', 200);
      })
      .then(posts => {
        log(posts, 'purple');
        log(`Done in ${Date.now() - start}ms`);
      })
      .catch(err => log('Error: ' + err.message, 'red'));
  });
</script>

5. Code Breakdown

Promise.all for parallel

All three fetches start simultaneously. Total time ≈ the slowest (600ms). The .then callback uses array destructuring to get all three results in order.

Sequential chain

Each .then returns the next promise. The chain waits for each before starting the next. Total time = sum of all (400 + 300 + 200 = 900ms) — much slower than parallel.

Single .catch for the whole chain

One .catch at the end handles any rejection from any step in the chain. If any promise rejects, execution jumps to catch — intermediate .then handlers are skipped.

6. Common Mistakes

Mistake 1 — Not returning the promise in .then

// Bug — doesn't chain, runs in parallel unexpectedly
fetchUser(1)
  .then(user => {
    fetchProfile(user.id); // forgot return! Next .then doesn't wait
  })
  .then(profile => {
    console.log(profile); // undefined — fetchProfile wasn't chained
  });

// Fix — always return from .then
.then(user => fetchProfile(user.id)) // returns the promise

Mistake 2 — Wrapping a promise in a new Promise unnecessarily

// Anti-pattern — "explicit promise construction anti-pattern"
function fetchUser(id) {
  return new Promise((resolve, reject) => {
    fetch(`/api/users/${id}`)
      .then(r => resolve(r.json()))
      .catch(reject);
  });
}

// Fix — fetch already returns a Promise — just return it
function fetchUser(id) {
  return fetch(`/api/users/${id}`).then(r => r.json());
}

Mistake 3 — Unhandled promise rejections

// Bug — no .catch — rejection goes unhandled (crashes in some environments)
fetchUser(99).then(user => console.log(user));

// Fix — always add .catch
fetchUser(99)
  .then(user => console.log(user))
  .catch(err => console.error(err));

7. Best Practices

  1. Always add .catch() to promise chains — unhandled rejections can silently fail.
  2. Always return promises inside .then() when chaining — otherwise the next .then doesn't wait.
  3. Use Promise.all for parallel operations when you need all results before proceeding.
  4. Use Promise.allSettled when partial failures are acceptable — batch uploads, optional data.
  5. Avoid the explicit construction anti-pattern — don't wrap existing promises in new Promise.
  6. Use async/await for sequential chains — it reads more like synchronous code (Lesson 3).

8. Practice Exercise

  1. Create three promise-returning functions that each resolve after different delays. Use Promise.all to run them in parallel and log all results when done.
  2. Chain three promises sequentially — each one uses the result of the previous. Add a .catch at the end. Simulate a failure in the middle and verify the catch fires.
  3. Implement a withTimeout(promise, ms) utility using Promise.race. Test with a promise that resolves after 2000ms but a 500ms timeout.

9. Assignment

Build a "Multi-Source Weather App" (simulated).

  1. Create three simulated fetch functions: getTemperature(city), getHumidity(city), getWindSpeed(city) — each returns a Promise that resolves after a random delay (200–800ms).
  2. Use Promise.all to fetch all three simultaneously and display a weather card when all resolve.
  3. Add a city input — fetching new data cancels/ignores results from the previous city (use a counter/token to detect stale responses).
  4. Add error handling — if any fetch fails, show which metric failed while still showing the others using Promise.allSettled.

Deliverable: One HTML file.

10. Interview Questions

  1. What is a Promise?
    An object representing the eventual result of an async operation. It can be pending (waiting), fulfilled (succeeded with a value), or rejected (failed with a reason). Once settled, the state never changes.
  2. What is the difference between Promise.all and Promise.allSettled?
    Promise.all rejects immediately if any promise rejects — all must succeed. Promise.allSettled always waits for all promises regardless of outcome, returning an array of {status, value/reason} objects for each.
  3. How do you handle errors in a promise chain?
    Add .catch() at the end of the chain. A single catch handles any rejection from any .then() in the chain above it. You can also add intermediate catches to recover from specific failures.
  4. What does returning a value in .then() do?
    It wraps the value in a new resolved Promise, which is passed to the next .then(). If you return a Promise, the chain waits for that Promise to settle before calling the next .then().

11. Additional Resources

  • MDN — Promise
  • javascript.info — Promises, async/await — multi-chapter guide
  • MDN — Promise.all() / allSettled() / race() / any()