Asynchronous JS, APIs, and JSON
Fetch and process data asynchronously, design and consume APIs, and add realtime features.
Content
Async await patterns
Versions:
Watch & Learn
AI-discovered learning video
Async/Await Patterns — Make Your Async Code Readable (and Less Cry-Inducing)
'This is the moment where the concept finally clicks.'
You're already comfortable with the Fetch API and promise chains from the previous lesson. Remember how promise chaining can feel like passing a baton across a relay team — neat, but a single fumble and everything collapses? Async/await is the elegant jogger who runs the whole race and looks like they're sipping a latte the whole time. It is built on promises, but it gives you synchronous-looking code that handles asynchronous work clearly.
What async/await actually is
- Async functions are functions that always return a promise. They let you use the
awaitkeyword inside them. awaitpauses execution inside the async function until the awaited promise settles. Then it returns the promise's resolved value (or throws if it rejected).
Think of async as the magic label that turns your function into a promise machine, and await as the polite waiter that hands you the result of a promise when it's ready.
Why this matters (and where you already saw the pieces)
- From 'Fetch API fundamentals' you know
fetch()returns a promise. - From 'Promises chaining and errors' you know how to
.then(...).catch(...)and how errors propagate.
Async/await is the same promise system — it doesn't replace it — but it makes complex promise chains much easier to read and reason about.
Quick pattern conversions: Promise chain → async/await
Promise chaining (previous lesson):
fetch('/api/user/42')
.then(response => response.json())
.then(user => fetch(`/api/posts?author=${user.id}`))
.then(resp => resp.json())
.then(posts => console.log(posts))
.catch(err => console.error('Error fetching user posts', err));
Same flow with async/await:
async function showUserPosts() {
try {
const response = await fetch('/api/user/42');
const user = await response.json();
const postsResp = await fetch(`/api/posts?author=${user.id}`);
const posts = await postsResp.json();
console.log(posts);
} catch (err) {
console.error('Error fetching user posts', err);
}
}
showUserPosts();
Micro explanation
- Each
awaitreplaces a.then(...)handler and lets you write steps in top-to-bottom order. try/catcharound await calls replaces.catch(...)and lets you handle errors nearby the code that throws.
Patterns and anti-patterns
1) Sequential awaits (when order matters)
If each step depends on the previous result, await them sequentially.
// good: second fetch needs data returned by the first
const userResp = await fetch('/api/user');
const user = await userResp.json();
const detailsResp = await fetch(`/api/details/${user.id}`);
const details = await detailsResp.json();
2) Parallel awaits (when steps are independent)
If tasks don't depend on each other, fire them off together and await them with Promise.all — this avoids unnecessary waiting.
// bad: does two independent fetches one after the other
const a = await fetch('/api/a');
const b = await fetch('/api/b');
// good: kick both off then wait for both
const [aResp, bResp] = await Promise.all([fetch('/api/a'), fetch('/api/b')]);
Why? Because await fetch('/api/a') finishes before you start fetch('/api/b') in the bad version. Parallelism wins when tasks are independent.
3) Mixing await in loops — watch out
Avoid forEach(async x => { await ... }) — forEach won't await async callbacks. Use for...of or map + Promise.all.
// good: sequential
for (const id of ids) {
await doWork(id);
}
// good: parallel
await Promise.all(ids.map(id => doWork(id)));
Error handling patterns
- Use
try/catchwithin your async function to catch rejections from awaited promises. - Re-throw if you want callers to handle it further up.
async function getJson(url) {
try {
const resp = await fetch(url);
if (!resp.ok) throw new Error('Network response not ok');
return await resp.json();
} catch (err) {
console.error('getJson failed', err);
throw err; // propagate
}
}
This mirrors the earlier promise .catch() behavior but places error handling near the code that causes it — much easier to reason about.
Advanced: cancellation with AbortController
Fetch doesn't magically cancel just because you abandon the promise. Use AbortController to stop requests when the user navigates away or input changes.
async function search(query, controller) {
try {
const resp = await fetch(`/api/search?q=${encodeURIComponent(query)}`, { signal: controller.signal });
return await resp.json();
} catch (err) {
if (err.name === 'AbortError') console.log('Search aborted');
else throw err;
}
}
// usage
const controller = new AbortController();
search('pizza', controller);
// later, if user types again
controller.abort();
This ties nicely to UI code from the DOM lesson — abort stale requests when the user types a new query.
Real-world tips (the stuff TAs whisper in exam prep)
- Prefer async/await for readability for multi-step logic. Use Promise chains for very simple flows if you prefer the chaining style.
- Always validate responses (check
resp.ok) before callingresp.json(). - For multiple independent requests, use
Promise.allorPromise.allSettledif you want partial results. - Top-level await exists in modern modules (ESM) — handy for scripts, but in browsers you often still wrap in an
asyncIIFE or a module script.
Example: async IIFE
(async function init() {
const config = await getJson('/api/config');
// do startup work
})();
Common misunderstandings
- 'await blocks the whole thread' — false.
awaitsuspends only the async function. The event loop keeps running. - 'async/await replaces promises' — false. It's syntactic sugar on promises.
- 'You must use try/catch for every await' — not always; sometimes you want rejections to bubble up. Use try/catch where you can handle or transform the error.
Quick cheatsheet
- Use
asyncon the function definition. - Use
awaitto get the promise's resolved value. - Wrap in
try/catchto catch rejections. - Use
Promise.allfor parallelism. - Use
AbortControllerfor canceling fetches.
Key takeaways
- Async/await is clearer syntax for working with promises — same engine, nicer code.
- Use sequential awaits when steps depend on one another; use Promise.all for independent tasks to gain concurrency.
- Handle errors with try/catch, and re-throw when you need higher-level handling.
- Cancel fetches with AbortController to keep your UI responsive and avoid race conditions.
Remember: the goal is readable, maintainable, and correct asynchronous code. Write like you're explaining it to your future self at 3am — because you'll likely be the one debugging it later.
Go forth and await like a pro. Your promise chains will thank you.
Comments (0)
Please sign in to leave a comment.
No comments yet. Be the first to comment!