Chapter 20 of 28 · JavaScript for Beginners — Complete Free Tutorial

Promises: Handling Async Operations

A Promise is an object representing a value that is not ready yet — a placeholder for the future result of an async operation. Promises turn nested callbacks into flat, chainable, error-propagating pipelines. They underpin fetch, async/await, timers-in-parallel and virtually every modern JavaScript API.

The three states

StateMeaning
pendingNot settled yet — work in progress
fulfilledSucceeded with a value (resolve)
rejectedFailed with a reason (reject)

Once settled (fulfilled or rejected) it never changes again — that immutability is what makes promises reliable.

Creating a promise

const wait = (ms) =>
  new Promise((resolve) => setTimeout(() => resolve('done'), ms));

wait(500).then((value) => console.log(value));   // → 'done' (after ~500ms)

const fail = new Promise((_, reject) => {
  reject(new Error('nope'));
});

The executor runs immediately and receives resolve/reject — call each at most once (later calls are ignored).

Consuming: then, catch, finally

wait(300)
  .then((value) => {
    console.log(value);          // fulfilled path
    return 'next value';         // next .then receives this
  })
  .catch((err) => {
    console.error('failed:', err.message);   // any rejection above lands here
  })
  .finally(() => {
    console.log('always — cleanup');         // runs on success OR failure
  });

Returning a value from .then passes it downstream; returning another promise chains — the next .then waits for it automatically.

Chaining beats nesting

// flat pipeline
getUser(1)
  .then((user) => getOrders(user.id))
  .then((orders) => total(orders))
  .then((sum) => console.log(sum))
  .catch((err) => console.error('any step failed:', err));   // one catch for ALL steps

Rejections skip intermediate .thens until a .catch — exactly like try/catch around the whole chain. Expected output: a readable top-to-bottom flow with one error sink at the bottom.

Promise.all: run in parallel, wait for all

const [user, posts, settings] = await Promise.all([
  fetchUser(1),
  fetchPosts(),
  fetchSettings(),
]);

All three start together; the result array keeps order. One rejection rejects everything — use allSettled when partial failure is acceptable:

const results = await Promise.allSettled([taskA(), taskB()]);
// → [{ status: 'fulfilled', value: 1 }, { status: 'rejected', reason: Error }]

Promise.race and Promise.any

// first to settle wins (commonly: a timeout)
const data = await Promise.race([
  fetch('/api/slow'),
  new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), 5000)),
]);

// first SUCCESS wins; rejects only if all fail
const best = await Promise.any([mirror1(), mirror2()]);

Utility constructors

Promise.resolve(42);            // already-fulfilled promise
Promise.reject(new Error('x')); // already-rejected
Promise.all([...]).then(…)      // combinators above

Useful for adapting non-promise code and testing consumers synchronously.

Mistakes to avoid in promise code

// ❌ forgetting to return — next .then does not wait for the fetch
getData().then((d) => {
  fetch('/save', { body: d });
}).then(() => console.log('saved'));   // logs before the save finishes!

// ✅ return the promise
getData().then((d) => fetch('/save', { body: d }))
         .then(() => console.log('saved'));

// ❌ swallowing errors silently
promise.catch(() => {});

// ❌ calling .catch on nothing — attach handlers when you create the promise,
//    or the rejection becomes “unhandled” and noisy in the console

Unhandled rejections

A rejected promise with no .catch prints “Unhandled promise rejection” — modern browsers surface this as an error. Always attach a handler at the point of consumption (or wrap in try/catch with await — next chapter).

Promise.all vs sequential awaits

// sequential: 1s + 1s + 1s = ~3s ❌ slow
const a = await stepA();
const b = await stepB();
const c = await stepC();

// parallel: max(1s, 1s, 1s) = ~1s ✅
const [a, b, c] = await Promise.all([stepA(), stepB(), stepC()]);

Rule: independent work → Promise.all; dependent work (B needs A) → sequential. This one habit halves page-load times in data-heavy apps.

Common mistakes

  • Nesting instead of chaining — return the inner promise to flatten.
  • Missing return in .then — downstream steps race ahead.
  • Using Promise.all when partial success is OK — one failure kills all; switch to allSettled.
  • Starting requests sequentially out of habit — parallelise independent calls.
  • Double-resolving — the second resolve/reject call is a no-op; structure executors once.

Practice checklist

  1. Wrap setTimeout in delay(ms) and chain three delays — measure total time.
  2. Convert a nested callback pyramid into a flat promise chain with one .catch.
  3. Race a fake fetch against a 2-second timeout and log which won.
  4. Compare sequential vs Promise.all timings for three 1-second fake tasks.

Promises are the machinery — async/await is the elegant syntax on top. Next: Async/Await.

Quiz: Promises: Handling Async Operations

5 questions · 70% to pass

Frequently Asked Questions

JavaScript is the programming language of the web. It adds behaviour to pages: responding to clicks and input, validating forms, fetching data from APIs, updating the DOM without reloading, and much more. It also runs on servers (Node.js), in desktop and mobile apps, and in build tooling.

They are unrelated languages that share a similar name for historical marketing reasons. Java is a statically-typed compiled language; JavaScript is a dynamically-typed interpreted language designed for the browser. Knowing one does not imply knowledge of the other.

Helpful but not mandatory — HTML gives you the structure JavaScript manipulates and CSS the styling it toggles. If you know basic tags, classes and selectors, you have enough to start; the DOM chapters assume only that much.

Use const by default and let when the value must change. Avoid var entirely in modern code: it is function-scoped instead of block-scoped, hoists differently, and can create accidental globals. This course uses const and let throughout.

The Document Object Model is the browser’s in-memory tree of objects representing your HTML. JavaScript reads and modifies that tree — selecting elements, changing text, adding nodes, attaching event listeners — and the browser re-renders the page to match.

== compares after converting types (so 5 == "5" is true), while === requires the same value AND the same type (5 === "5" is false). Loose equality has famously confusing rules — use === and !== everywhere unless you have a specific reason not to.

JavaScript is single-threaded, so long-running work (timers, fetches, events) is handled by the browser and its callbacks are queued. The event loop moves queued callbacks onto the call stack only when it is empty, with promise callbacks (microtasks) taking priority over timers. It is why asynchronous code never freezes the page.

A Promise is an object representing a future value — pending, fulfilled or rejected — that can be chained and combined (Promise.all). async/await is syntax sugar over promises that lets asynchronous code read top-to-bottom with ordinary try/catch error handling.

Yes. Frameworks are built on exactly these fundamentals — components are functions, state is your variable discipline, effects are events, rendering is DOM work. Developers who learn vanilla first understand what the framework is doing and debug far faster.

Type out the examples instead of copying, build small end-to-end projects (expense tracker, weather app, quiz), handle every UI state (loading, empty, success, error), and revisit fundamentals while reading real code. Consistency beats volume — a little most days outpaces occasional marathons.