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
- Promise states & lifecycle
- Chaining & transformation pipelines
- Error propagation & recovery
- Parallel execution with Promise.all
- Race conditions & Promise.any
- Real-world retry & fallback patterns
π‘ 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:
- Download Node.js to run JavaScript on your computer
- Use your browser's Developer Console (Press F12) to test code snippets
- Create a .html file with <script> tags and open it in your browser
π¬ Real-World Analogy: A Promise is like ordering food at a restaurant:
- β’ Pending = Your order is being prepared (waiting)
- β’ Fulfilled = Your food arrives (success!)
- β’ Rejected = Kitchen is out of ingredients (error)
- β’ .then() = "When my food arrives, bring me ketchup"
- β’ .catch() = "If something goes wrong, tell me what happened"
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 State | Meaning | What Happens Next |
|---|---|---|
| Pending | Still waiting for result | Nothing yet β keep waiting |
| Fulfilled | Operation succeeded | .then() handlers run |
| Rejected | Operation 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():
- .then(() => "value") β instantly wraps the value in a Promise
- .then(() => fetch("/data")) β waits for the returned Promise to settle
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
- Mixing callbacks with Promises β Always choose one system
- Blocking async with sync work β Any heavy CPU work blocks the event loop
- Creating Promise pyramids β Callback hell but with Promises
- Using async where not needed β Extra async functions add unnecessary microtasks
- Returning inside .catch() unintentionally β This suppresses errors
π₯ 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
// DoneThis is how Anthropic, OpenAI, Mistral, Groq maximize GPU throughput and handle real-world failures gracefully.
π― Key Takeaways
- Promise chains create predictable async pipelines
- Error propagation allows centralized error handling
- Promise.all(), Promise.allSettled(), and Promise.any() provide different concurrency patterns
- Throttling and pooling prevent system overload
- Proper error handling with retry and fallback strategies is essential for production apps
- Avoid common anti-patterns like forgetting to return or creating Promise pyramids
- Understanding these patterns is critical for building scalable, reliable applications
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
- Previous: Event Loop, Microtasks & Async Internals
- Next: Async/Await Architecture & Best Practices β Patterns for concurrent requests, cancellation, and error recovery
- Quick reference: JavaScript cheat sheet
- From the blog: Async/Await in JavaScript Explained