Mastering Promises

Reviewed & published by Brayan K

A Promise is a JavaScript object representing the eventual result of an asynchronous operation, starting in a pending state and later settling as either fulfilled with a value or rejected with an error.

Part of the free JavaScript course at LearnCodingFast β€” hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

What You'll Learn in This Lesson

πŸ’‘ Running Code Locally: While this online editor runs real JavaScript, some advanced examples (like fetch to external APIs) may have limitations. For the best experience:

πŸ“¬ Real-World Analogy: A Promise is like ordering food at a restaurant:

JavaScript Promises are the foundation of all modern asynchronous programming. Nearly every API you interact withβ€”fetch(), database queries, file I/O, animations, WebSockets, workers, AI inference calls, and mobile frameworksβ€”runs on top of Promises. Mastering Promises is the difference between writing buggy, unpredictable code and building systems that scale smoothly across browsers, servers, frameworks, and real-world applications.

Promise StateMeaningWhat Happens Next
PendingStill waiting for resultNothing yet β€” keep waiting
FulfilledOperation succeeded.then() handlers run
RejectedOperation failed.catch() handlers run

At their core, Promises represent a value that will exist in the future. Beginners think of them as "callbacks but cleaner," but this mindset misses their true power. Promises behave like mini-state machines that move between three statesβ€”pending, fulfilled, and rejectedβ€”and every .then() creates a new Promise linked by a chain of microtasks.

πŸ”₯ Basic Promise Chaining

A Promise chain always returns a new Promise, which allows operations to form asynchronous pipelines. Behind the scenes, JavaScript pushes callback functions into the microtask queue, ensuring they run before rendering or macrotask events like timers.

Promise.resolve("Start")
  .then(msg => {
    console.log(msg);
    return "Next";
  })
  .then(next => {
    console.log(next);
    return "Final";
  })
  .then(final => console.log(final));

Output: Start Next Final. Each .then() creates a microtask that executes immediately after the current script finishes.

πŸ”₯ Returning Promises vs Returning Values

A critical difference exists between returning a value and returning a Promise inside .then():

Promise.resolve()
  .then(() => {
    return fetch("/api"); // chained asynchronously
  })
  .then(res => res.json())
  .then(data => console.log(data));

The entire chain waits until the inner Promise completes. This allows clean, readable async pipelines where each step waits for the previous operation.

πŸ”₯ Error Propagation β€” The Most Important Promise Mechanic

If you don't understand error flow, you don't understand Promises. Errors flow down the chain until a .catch() handles them. This allows centralised error handling in large applications.

Promise.resolve()
  .then(() => {
    throw new Error("Failure");
  })
  .catch(err => {
    console.log("Handled:", err.message);
  });

The powerful insight: Once .catch() runs, the chain becomes "clean" again unless you throw another error.

Promise.resolve()
  .then(() => {
    throw "First error";
  })
  .catch(err => {
    console.log("Caught:", err);
    return "Recovered";
  })
  .then(msg => console.log(msg));

Output: Caught: First error Recovered. The Promise continues normally after error recovery. This behaviour is essential for retry logic, fallbacks, alternative flows, and graceful degradation.

πŸ”₯ Throwing Errors Inside .catch() Creates a New Failure Chain

Promise.resolve()
  .then(() => {
    throw "Something broke";
  })
  .catch(err => {
    console.log("Handled", err);
    throw "New error"; // rethrowing
  })
  .catch(err => console.log("Final handler:", err));

Output: Handled Something broke Final handler: New error. This propagation system enables multi-phase validation, authentication flows, and structured error pipelines.

πŸ”₯ Promise Chaining Enables Data Transformation Pipelines

Here's what the chain achieves: GET user β†’ Parse JSON β†’ Extract ID β†’ GET posts β†’ Parse JSON β†’ Filter published posts β†’ Log results. No nesting. No callback hell. Clean, readable async sequences.

πŸ”₯ Running Promises in Parallel (Promise.all)

Promise.all() lets you execute multiple async operations at the same time. It is faster and more efficient than waiting for each Promise step-by-step.

const fetchUser = fetch("/api/user");
const fetchPosts = fetch("/api/posts");
const fetchMessages = fetch("/api/messages");

Promise.all([fetchUser, fetchPosts, fetchMessages])
  .then(async ([userRes, postsRes, msgRes]) => {
    const user = await userRes.json();
    const posts = await postsRes.json();
    const messages = await msgRes.json();
    return { user, posts, messages };
  })
  .then(data => console.log("Loaded:", data))
  .catch(err => console.error("Failed:", err));

Key features: If any Promise fails β†’ the entire chain rejects. It's perfect for loading dashboards, games, financial data, or startup screens. Much faster than awaiting each request one-by-one.

πŸ”₯ Promise.allSettled() β€” Wait For Everything

Unlike Promise.all(), Promise.allSettled() waits for EVERY Promise to finish, even if some fail:

Used in: analytics batching, bulk database updates, large game state sync, uploading multiple assets (some may fail but progress continues).

πŸ”₯ Promise.any() β€” First Success Wins

Returns the first successful result, ignores failures. Useful for redundancy: multiple CDNs, multiple AI model endpoints, or fallback servers.

Promise.any([
  fetch("https://server1/model"),
  fetch("https://server2/model"),
  fetch("https://backup.model")
])
.then(res => console.log("Fastest model:", res))
.catch(() => console.error("Every model endpoint failed"));

This pattern is used by real production apps to stay up even if half their services fail.

πŸ”₯ Promise Chaining Patterns for Clean Architecture

Complex apps break their async logic into pipelines. Here is a real-world pattern:

const pipeline = (input) =>
  Promise.resolve(input)
    .then(sanitize)
    .then(validate)
    .then(convert)
    .then(store)
    .then(notifyUser)
    .catch(handleFailure);

function sanitize(data) { /* ... */ return data; }
function validate(data) { /* ... */ return data; }
function convert(data) { /* ... */ return data; }
function store(data) { /* ... */ return data; }
function notifyUser(data) { /* ... */ return data; }

This structure is used everywhere: uploading videos, sending content to LLM models, saving game progress, processing payments, managing user onboarding. Good Promise structure = stable, maintainable, scalable software.

πŸ”₯ Throttling Promises (Controlling Load)

Sometimes you can't run everything at once. You may need to limit concurrency to protect an API, your server, your database, your GPU/LLM endpoint, or your user's device.

function throttle(tasks, limit) {
  let active = 0;
  let index = 0;

  return new Promise(resolve => {
    const results = [];

    function next() {
      if (index === tasks.length && active === 0) {
        return resolve(results);
      }

      while (active < limit && index < tasks.length) {
        const current = index++;
        active++;

        tasks[current]()
          .then(res => results[current] = res)
          .catch(err => results[current] = err)
          .finally(() => {
            active--;
            next();
          });
      }
    }

    next();
  });
}

// Usage
throttle(
  [task1, task2, task3, task4, task5],
  2 // run only 2 at a time
).then(console.log);

This pattern is used in: image processing, AI inference batching, uploading multiple videos, e-commerce product updates, database-intensive operations.

πŸ”₯ Promise Cancellation Patterns

JavaScript doesn't have built-in Promise cancellation, but you can design cancellation-aware flows using AbortController:

const controller = new AbortController();

fetch("/data", { signal: controller.signal })
  .then(res => res.json())
  .then(console.log)
  .catch(err => {
    if (err.name === "AbortError") {
      console.log("Fetch cancelled");
    }
  });

// Later:
controller.abort();

Apps use this to: cancel old search requests, cancel loading screens if user navigates away, cancel long API calls (e.g., AI generation).

πŸ”₯ Fallback Pattern

If main API fails, fallback to backup:

fetch("/primary")
  .catch(() => fetch("/backup"))
  .then(res => res.json())
  .then(console.log);

πŸ”₯ Retry With Backoff

function retry(fn, retries = 3, delay = 500) {
  return fn().catch(err => {
    if (retries <= 0) throw err;
    return new Promise(res => setTimeout(res, delay))
      .then(() => retry(fn, retries - 1, delay * 2));
  });
}

// Usage
retry(() => fetch("/unstable-api"), 5, 1000)
  .then(res => res.json())
  .then(console.log)
  .catch(err => console.error("Failed after retries:", err));

Used heavily in: AI inference (HuggingFace, OpenAI, Anthropic), unstable network connections, mobile apps, payment processing gateways.

πŸ”₯ The Promise Pool Pattern

Used by Cloudflare, AWS Lambda workers, GPU batching for LLMs, web scrapers, and payment processing. A Promise Pool ensures you run N tasks at a time:

This pattern prevents: API rate limit bans, GPU overload, server memory spikes.

πŸ”₯ Common Promise Mistakes to Avoid

❌ Anti-Patterns

πŸ”₯ The Correct Way to Chain

doA()
  .then(() => {
    doB().then(() => {
      doC().then(() => {});
    });
  });
doA()
  .then(() => doB())
  .then(() => doC());

πŸ”₯ AI/LLM Pipeline Pattern

For AI systems, multi-stage processing is essential:

async function llmPipeline(input) {
  const embedding = await embed(input);
  const context = await fetchContext(embedding);
  const draft = await generateDraft(context);
  return await refine(draft);
}

// With error handling
async function robustLLMPipeline(input) {
  try {
    const embedding = await retry(() => embed(input));
    const context = await fetchContext(embedding);
    const draft = await retry(() => generateDraft(context));
    return await refine(draft);
  } catch (err) {
    console.error("LLM pipeline failed:", err);
    return fallbackResponse(input);
  }
}
// 🎯 YOUR TURN β€” replace each ___ using the hint beside it.

function delay(ms, value) {
  // 1) A promise is settled by calling this, not by returning.
  return new Promise((resolve) => setTimeout(() => ___(value), ms));  // πŸ‘‰ replace ___ with resolve
}

function mightFail(ok) {
  return new Promise((resolve, reject) => {
    if (ok) resolve("worked");
    // 2) The other way out: hand back an Error.
    else ___(new Error("nope"));            // πŸ‘‰ replace ___ with reject
  });
}

async function main() {
  // 3) Pause here until the promise settles, then take its value.
  const first = ___ delay(10, "one");       // πŸ‘‰ replace ___ with await
  console.log("Awaited:", first);

  // 4) Wait for EVERY one, and get an array in the same order you passed them.
  const both = await Promise.___([delay(5, "a"), delay(1, "b")]);   // πŸ‘‰ replace ___ with all
  console.log("All:", both);

  // 5) Wait for the FIRST to settle and ignore the rest.
  const winner = await Promise.___([delay(50, "slow"), delay(1, "fast")]);  // πŸ‘‰ replace ___ with race
  console.log("Race:", winner);

  try {
    await mightFail(false);
  } catch (err) {
    console.log("Caught:", err.message);
  }

  // allSettled never rejects: it reports on all of them either way.
  const settled = await Promise.allSettled([mightFail(true), mightFail(false)]);
  console.log("Settled:", settled.map((s) => s.status).join(", "));
}

main().then(() => console.log("Done"));

// βœ… Expected output:
// Awaited: one
// All: [ 'a', 'b' ]
// Race: fast
// Caught: nope
// Settled: fulfilled, rejected
// Done

This is how Anthropic, OpenAI, Mistral, Groq maximize GPU throughput and handle real-world failures gracefully.

🎯 Key Takeaways

Practice quiz

What are the three states of a Promise?

  • start, running, done
  • open, closed, error
  • pending, fulfilled, rejected
  • waiting, success, retry

Answer: pending, fulfilled, rejected. A Promise begins pending and settles as either fulfilled (with a value) or rejected (with an error).

Which handler runs when a Promise is fulfilled?

  • .then()
  • .catch()
  • .finally()
  • .reject()

Answer: .then(). Per the lesson's state table, fulfilled Promises run .then() handlers; rejected ones run .catch().

In a chain, errors flow down until what handles them?

  • A .then()
  • A try block
  • The event loop
  • A .catch()

Answer: A .catch(). Errors propagate down the chain until a .catch() handles them, enabling centralized error handling.

After a .catch() returns a value, what happens to the chain?

  • It stops permanently
  • It becomes 'clean' and continues with following .then()s
  • It rejects again
  • It restarts from the top

Answer: It becomes 'clean' and continues with following .then()s. Once .catch() recovers, the chain continues normally unless you throw another error.

What does returning a value from a .then() callback do?

  • Wraps the value in a Promise for the next .then()
  • Ends the chain
  • Throws an error
  • Skips the next .then()

Answer: Wraps the value in a Promise for the next .then(). Returning a value instantly wraps it in a resolved Promise, passing it to the next .then().

If one Promise in Promise.all([...]) rejects, what happens?

  • The others' results are still returned
  • It waits and ignores the failure
  • The entire Promise.all rejects
  • It retries automatically

Answer: The entire Promise.all rejects. Promise.all rejects as soon as any input Promise rejects; for partial results use Promise.allSettled.

How does Promise.allSettled differ from Promise.all?

  • It rejects faster
  • It waits for every Promise to finish, even failures
  • It only takes two Promises
  • It returns the first success

Answer: It waits for every Promise to finish, even failures. allSettled waits for all Promises and reports each as fulfilled or rejected, never short-circuiting.

What does Promise.any return?

  • All results as an array
  • The first rejection
  • Always undefined
  • The first successful result, ignoring failures

Answer: The first successful result, ignoring failures. Promise.any resolves with the first fulfilled result and ignores rejections β€” useful for redundant endpoints.

Which is a common Promise anti-pattern the lesson warns about?

  • Returning values from .then()
  • Forgetting to return inside .then()
  • Using .catch() at the end
  • Chaining .then() calls

Answer: Forgetting to return inside .then(). Forgetting to return inside .then() yields undefined to the next step and breaks the data flow.

What does JavaScript use to support Promise cancellation?

  • Promise.cancel()
  • clearPromise()
  • AbortController
  • setTimeout

Answer: AbortController. There is no built-in cancellation; you design cancellation-aware flows with AbortController and its signal.

Continue this course