Mastering Promises: Chaining, Error Flow & Patterns
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
.htmlfile 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.
Basic Promise Chaining
See how each .then() creates a microtask that executes in sequence
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
Returning Promises vs Values
The chain waits for returned Promises 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.
Error Propagation
Errors flow down the chain until a .catch() handles them
Promise.resolve()
.then(() => {
throw new Error("Failure");
})
.catch(err => {
console.log("Handled:", err.message);
});Output: Handled: Failure
The powerful insight: Once .catch() runs, the chain becomes "clean" again unless you throw another error.
Error Recovery
The chain continues normally after .catch() recovers
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
Throwing Errors Inside .catch()
Rethrowing 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
Data Transformation Pipelines
Clean, readable async sequences without nesting
fetch("/user")
.then(res => res.json())
.then(user => user.id)
.then(id => fetch(`/user/${id}/posts`))
.then(res => res.json())
.then(posts => posts.filter(p => p.published))
.then(final => console.log(final));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.
Running Promises in Parallel
Promise.all() executes multiple async operations simultaneously
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:
Promise.allSettled()
Wait for EVERY Promise to finish, even if some fail
Promise.allSettled([
fetch("/a"),
fetch("/b"),
fetch("/c")
]).then(results => {
results.forEach((result, i) => {
if (result.status === "fulfilled") {
console.log(`Task ${i} succeeded:`, result.value);
} else {
console.log(`Task ${i} failed:`, result.reason);
}
});
});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()
Returns the first successful result, ignores failures
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:
Promise Pipeline Pattern
Break async logic into clean, maintainable pipelines
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.
Throttling Promises
Limit concurrency to protect APIs and servers
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(() => {
...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:
Promise Cancellation
Use AbortController to cancel async operations
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:
Fallback Pattern
If main API fails, fallback to backup
fetch("/primary")
.catch(() => fetch("/backup"))
.then(res => res.json())
.then(console.log);🔥 Retry With Backoff
Retry With Backoff
Automatically retry failed operations with exponential delay
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:
The Promise Pool Pattern
Run N tasks at a time to prevent system overload
async function promisePool(tasks, limit = 4) {
const results = [];
let i = 0;
const run = async () => {
while (i < tasks.length) {
const idx = i++;
try {
results[idx] = await tasks[idx]();
} catch (err) {
results[idx] = err;
}
}
};
const workers = Array.from({ length: limit }, run);
await Promise.all(workers);
return results;
}
// Example usage
const urls = [...Array(50)].map((_, i) => `/data/${i}`);
const tasks = urls.map(url =>
...This pattern prevents: API rate limit bans, GPU overload, server memory spikes.
🔥 Common Promise Mistakes to Avoid
❌ Anti-Patterns
- Forgetting to return inside .then() —
.then(x => { process(x); })returns undefined - 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
Wrong:
Wrong Chaining (Anti-Pattern)
Avoid Promise pyramids - this is callback hell with Promises
doA()
.then(() => {
doB().then(() => {
doC().then(() => {});
});
});Right:
Correct Chaining
Flat, readable chains without nesting
doA()
.then(() => doB())
.then(() => doC());🔥 AI/LLM Pipeline Pattern
For AI systems, multi-stage processing is essential:
AI/LLM Pipeline Pattern
Multi-stage processing for AI systems with error handling
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) {
co
...This 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(), andPromise.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
Sign up for free to track which lessons you've completed and get learning reminders.