Asynchronous JS, APIs, and JSON
Fetch and process data asynchronously, design and consume APIs, and add realtime features.
Content
Promises chaining and errors
Versions:
Watch & Learn
AI-discovered learning video
Sign in to watch the learning video for this topic.
Promises chaining and errors — JavaScript guide for CS50 Web
Ever had a fetch call quietly fail while your UI smugly showed a spinner forever? Welcome to the emotional rollercoaster of Promise chaining and error handling.
You already know how to use the Fetch API from the previous topic, and you've played with template literals and destructuring to make your code readable. Now we take those building blocks and learn how promises talk to each other — and how to politely (or ruthlessly) handle when one of them throws a tantrum.
What this lesson covers
- How promise chaining really works (not just the eye candy
.then().then()) - How errors propagate through chains and common pitfalls that make errors disappear like socks in a laundry machine
- Practical patterns using fetch, destructuring, and even custom Error classes
- Quick conversions to async/await — because sometimes the prose needs to be less poetic
This is a progression from DOM interaction and Fetch basics — now we focus on control flow: what happens when multiple async steps depend on each other.
Quick refresher (one-liner)
- A
.then()callback that returns a value becomes a resolved promise with that value. - A
.then()callback that returns a promise waits for it — that’s the chaining magic. - Throwing inside
.then()or returningPromise.reject(...)results in a rejection that travels down the chain until a.catch()is found.
Micro explanation
Think of each .then() as a conveyor belt station: pass a product (value) on, or throw it off (error). If you launch a new conveyor (return a promise), the next station waits for that new conveyor to finish.
Chaining example with fetch and destructuring
// Get user -> get profile -> log profile, robustly handling errors
fetch('/api/users/1')
.then(response => {
if (!response.ok) throw new Error(`HTTP ${response.status}`); // stop chain
return response.json(); // returns a promise that resolves with parsed JSON
})
.then(({ username }) => fetch(`/api/profiles/${username}`)) // return the next promise
.then(profileRes => {
if (!profileRes.ok) throw new Error('Profile fetch failed');
return profileRes.json();
})
.then(profile => console.log('Profile:', profile))
.catch(err => console.error('Chain failed:', err));
Notes:
- We used destructuring in
.then(({ username }) => ...)(remember that from the previous module). - We return the fetch for the profile so the chain waits for it. Not returning is a common bug.
Common mistakes and how to fix them
- Not returning a promise inside
.then
// Bad: inner fetch result is lost
fetch('/api/users/1')
.then(res => res.json())
.then(user => { fetch(`/api/profile/${user.id}`); }) // forgot to return
.then(profile => console.log(profile)) // profile is undefined
.catch(err => console.error(err));
Fix: return the inner promise.
- Swallowing errors by placing
.catch()too soon
fetch('/a')
.then(res => res.json())
.catch(err => { console.warn('Fetch failed, but continuing'); })
.then(data => doSomething(data)); // data might be undefined
If the .catch() doesn't rethrow or return a fallback, the chain continues with undefined. That can produce subtle bugs. If you intend to recover, return a fallback; otherwise rethrow.
- Nested
.then()(anti-pattern)
Nesting leads to spaghetti logic. Prefer returning promises and chaining.
Error propagation: throw vs return Promise.reject()
throw new Error('x')inside.then()is identical toreturn Promise.reject(new Error('x'))in terms of propagation.- Any thrown error or rejection will skip downstream
.then()handlers until it finds a.catch().
Example of recovery mid-chain:
fetch('/maybe')
.then(res => { if (!res.ok) throw new Error('nope'); return res.json(); })
.catch(err => {
console.warn('Recovering from error, providing default');
return { default: true }; // chain continues with this value
})
.then(data => console.log('Data (maybe fallback):', data));
This pattern is useful when you want to recover and continue; otherwise rethrow to let a later .catch() handle it.
Advanced tip: custom Error classes and instanceof
If you build larger apps, define custom errors so your .catch() can handle different problems differently.
class APIError extends Error {}
fetch('/api')
.then(res => { if (!res.ok) throw new APIError('API down'); return res.json(); })
.catch(err => {
if (err instanceof APIError) { /* handle API error */ }
else { throw err; } // rethrow unknown errors
});
This builds on your knowledge of JS classes and inheritance (remember that lecture!).
Async/await: same mechanics, different clothes
Promises chaining is the engine; async/await is a nicer dashboard.
async function loadProfile() {
try {
const res = await fetch('/api/users/1');
if (!res.ok) throw new Error('User fetch failed');
const { username } = await res.json();
const profileRes = await fetch(`/api/profiles/${username}`);
if (!profileRes.ok) throw new Error('Profile fetch failed');
const profile = await profileRes.json();
console.log(profile);
} catch (err) {
console.error('Error:', err);
}
}
Errors thrown inside async functions behave like rejected promises and are caught by the catch block. Same propagation semantics, cleaner syntax.
Quick debugging checklist
- Use the Network tab: is the request sent? what's the status?
- Check
response.okbefore parsing JSON — trying to parse an HTML error page as JSON is a classic facepalm. - Add
console.error(err)in.catch()orcatchblocks — don't silently ignore. - Listen for unhandled rejections in dev:
window.addEventListener('unhandledrejection', e => console.error('Unhandled rejection:', e.reason));
Key takeaways
- Always return promises from
.then()when you want to chain async work. - Throw or return Promise.reject to signal errors; they propagate until a
.catch. - Use
.catch()at the right place — either to recover (and return a fallback) or to handle terminal errors. - Async/await makes chains look synchronous but follows the same propagation rules.
- Use custom Error classes (remember classes & inheritance) for clearer error handling.
Here's the mental image: Promise chains are a relay race. If a runner trips (throws), the baton (value) doesn't magically appear at the finish line — someone must catch it, dust it off, and keep running, or call the medics (your
.catch).
Remember: Promises don't just make async code possible — they let you compose logic cleanly. Respect the return, don't swallow errors, and your UI will stop ghosting your users.
Happy chaining. Go break things gracefully.
Comments (0)
Please sign in to leave a comment.
No comments yet. Be the first to comment!