DEV Community

Timevolt
Timevolt

Posted on

Async/await: The Matrix of JavaScript

The Quest Begins (The “Why”)

I was knee‑deep in a Node.js service that fetched data from three different APIs, mashed the results together, and then wrote a summary to a database. The original code looked like a tangled mess of .then() callbacks, error‑handling spaghetti, and a couple of sneaky process.exit(1) calls that made debugging feel like defusing a bomb while blindfolded.

Every time I added a new endpoint I had to wrap the whole thing in an async IIFE just to use await at the top level. I kept asking myself: “Why does JavaScript make me jump through hoops just to write sequential‑looking async code?” It felt like I was stuck in a tutorial loop, watching the same “promise chain” video over and over.

That frustration sparked a mini‑quest: uncover the hidden gems of async/await that most tutorials skim over, the ones that can shave hours off debugging and make your code read like a story instead of a puzzle.

The Revelation (The Insight)

Turns out, the language already gave us a few powerful tricks that many developers either don’t know exist or dismiss as “nice‑to‑have”. Mastering them changes the way you think about asynchronous flow:

  1. Top‑level await – you can finally ditch the async IIFE boilerplate.
  2. for await…of – looping over async iterables becomes as natural as a regular for…of.
  3. Promise.allSettled + await – wait for all promises to settle, whether they resolve or reject, without the early‑exit surprise of Promise.all.

Each of these solves a real‑world gotcha that silently eats up your time. Let’s see them in action.

Wielding the Power (Code & Examples)

1. Top‑level await – No More Async IIFE

Before (the struggle)

// old.js
(async () => {
  const user = await fetchUser(42);
  const posts = await fetchPosts(user.id);
  const summary = await buildSummary(user, posts);
  await saveSummary(summary);
})(); // <-- yep, we still need this wrapper
Enter fullscreen mode Exit fullscreen mode

If you forget the IIFE, you get a SyntaxError because await isn’t allowed at the top level of a module. The wrapper adds visual noise and can confuse newcomers who wonder why the function is never called elsewhere.

After (the victory)

// new.js  (ES2022+)
const user = await fetchUser(42);
const posts = await fetchPosts(user.id);
const summary = await buildSummary(user, posts);
await saveSummary(summary);
Enter fullscreen mode Exit fullscreen mode

No extra parentheses, no extra indentation—just straight‑line code that reads like a recipe. The module itself becomes async, and Node (or modern browsers) handles the promise resolution automatically.

Why it matters: You spend less time wrapping/unwrapping code, and your intent is crystal clear: “do these steps in order, waiting for each”.

2. for await…of – Async Iterables Made Easy

Imagine you’re reading lines from a huge log file that’s streamed as an async iterator (think fs.createReadStream with readline). Using a normal for…of would give you promises, not the actual lines.

Before (the trap)

const stream = getLogStream(); // returns an async iterable
for (const linePromise of stream) {
  // linePromise is a Promise, not the line text
  linePromise.then(line => processLine(line)); // easy to forget .then()
}
Enter fullscreen mode Exit fullscreen mode

If you forget to await each linePromise, you end up processing pending promises, and your program may finish before any line is actually handled—a classic “fire‑and‑forget” bug.

After (the power)

const stream = getLogStream(); // async iterable
for await (const line of stream) {
  processLine(line); // line is already resolved!
}
Enter fullscreen mode Exit fullscreen mode

The await inside the loop automatically waits for each yielded promise before moving to the next iteration. It’s like having a personal assistant who hands you the next item only when it’s ready.

Why it matters: You avoid the subtle bug of mixing sync loops with async sources, and your code stays readable even when dealing with streams, paginated API cursors, or WebSocket message iterators.

3. Promise.allSettled + await – Wait for All, No Early Bail

Often we need to fire off several independent requests and collect every result, even if some fail. Using Promise.all throws away the whole batch as soon as one promise rejects, which can leave you with half‑baked data and a frantic try/catch.

Before (the struggle)

const [users, posts, comments] = await Promise.all([
  fetchUsers(),
  fetchPosts(),
  fetchComments()
]); // If fetchComments() fails, we never see users or posts
Enter fullscreen mode Exit fullscreen mode

If the comments service is down, you lose the users and posts data entirely—forcing you to restructure the call or write repetitive error‑handling logic.

After (the victory)

const results = await Promise.allSettled([
  fetchUsers(),
  fetchPosts(),
  fetchComments()
]);

const users = results[0].status === 'fulfilled' ? results[0].value : [];
const posts = results[1].status === 'fulfilled' ? results[1].value : [];
const comments = results[2].status === 'fulfilled' ? results[2].value : [];

// Now you have whatever succeeded, and you can log the failures
results.forEach((r, i) => {
  if (r.status === 'rejected') {
    console.error(`Request ${i} failed:`, r.reason);
  }
});
Enter fullscreen mode Exit fullscreen mode

Promise.allSettled waits for every promise to settle—whether they fulfill or reject—giving you an array of objects that tell you exactly what happened. Pair it with await and you get a clean, synchronous‑looking flow that still respects async reality.

Why it matters: You gain resilience. Partial failures no longer wipe out your entire dataset, and you can handle each outcome individually without nesting try/catch blocks everywhere.

Why This New Power Matters

Mastering these three patterns does more than save keystrokes—it reshapes how you reason about asynchronous code:

  • Top‑level await lets you treat a module like a script, removing ceremony and making the entry point obvious.
  • for await…of turns async iteration into a first‑class citizen, so you stop mixing for…of with .then() callbacks.
  • Promise.allSettled + await gives you a reliable way to gather data from multiple sources, turning what used to be a brittle “all‑or‑nothing” gamble into a tolerant, observable process.

When you internalize these tricks, you spend less time chasing subtle bugs and more time building features that actually delight users. You’ll notice your pull requests getting smaller, your code reviews smoother, and that satisfying “it just works” feeling creeping in more often.

Your Turn – Embark on Your Own Quest

Try refactoring a piece of your own code that currently relies on a tangled promise chain or an unnecessary async IIFE. Apply one (or all) of the patterns above and see how the readability and robustness change.

What’s the most surprising async/await feature you’ve discovered lately? Drop a comment below—I’d love to hear about your own victories in the async arena! 🚀

Top comments (0)