DEV Community

Orbit Websites
Orbit Websites

Posted on

Mastering Async/Await in JavaScript: A Practical Guide to Efficient Coding

Mastering Async/Await in JavaScript: A Practical Guide to Efficient Coding

Async/await isn’t magic — but it sure feels like it when you’re knee-deep in callback hell. JavaScript’s single-threaded nature means we rely heavily on asynchronous operations, and async/await gives us a clean, readable way to handle promises without losing our minds. If you're still nesting .then() chains or writing IIFEs around fetch, it’s time to level up.

Let’s cut through the noise and focus on how to use async/await effectively — with real code, common pitfalls, and patterns that scale.


1. Understand What Async/Await Actually Is

async/await is syntactic sugar over promises. That’s it. An async function always returns a promise, and await pauses execution until that promise resolves.

async function getData() {
  const res = await fetch('/api/data');
  const data = await res.json();
  return data;
}

// This returns a promise
getData().then(data => console.log(data));
Enter fullscreen mode Exit fullscreen mode

No callbacks. No .then() chains. Just linear-looking code that behaves asynchronously.

Key point: await only works inside async functions. Trying to use it at the top level (outside a module or function) will throw an error — unless you're in a modern environment with top-level await (ES modules in Node.js or browsers).


2. Handle Errors Like a Pro (Don’t Ignore Rejections)

One of the biggest mistakes? Forgetting that await can throw.

async function badExample() {
  const res = await fetch('/api/data'); // What if the network fails?
  const data = await res.json();
  return data;
}
Enter fullscreen mode Exit fullscreen mode

If the request fails, this function throws — and if you don’t catch it, your app crashes or silently fails.

Use try/catch:

async function getSafeData() {
  try {
    const res = await fetch('/api/data');
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    const data = await res.json();
    return data;
  } catch (err) {
    console.error('Fetch failed:', err);
    return null; // or throw, depending on your needs
  }
}
Enter fullscreen mode Exit fullscreen mode

Or, if you want to propagate the error up:

async function getStrictData() {
  const res = await fetch('/api/data');
  if (!res.ok) throw new Error(`Failed to fetch: ${res.status}`);
  return res.json();
}

// Caller handles the error
getStrictData().catch(err => console.log('Handled:', err));
Enter fullscreen mode Exit fullscreen mode

Pro tip: Wrap third-party API calls in try/catch. Assume everything can (and will) fail.


3. Don’t Block the Event Loop — Run in Parallel When You Can

await is sequential. If you await three independent API calls one after another, you’re adding up their latencies.

// SLOW: Each call waits for the previous
async function sequentialCalls() {
  const user = await fetch('/api/user').then(r => r.json());
  const posts = await fetch('/api/posts').then(r => r.json());
  const comments = await fetch('/api/comments').then(r => r.json());

  return { user, posts, comments };
}
Enter fullscreen mode Exit fullscreen mode

Instead, start all requests at once and await them together:

// FAST: All requests fire immediately
async function parallelCalls() {
  const [user, posts, comments] = await Promise.all([
    fetch('/api/user').then(r => r.json()),
    fetch('/api/posts').then(r => r.json()),
    fetch('/api/comments').then(r => r.json())
  ]);

  return { user, posts, comments };
}
Enter fullscreen mode Exit fullscreen mode

Promise.all() fails fast — if one promise rejects, the whole thing rejects. If you need results even when some fail, use Promise.allSettled():

const results = await Promise.allSettled([
  fetch('/api/user').then(r => r.json()),
  fetch('/api/broken').then(r => r.json()) // might fail
]);

results.forEach((result, i) => {
  if (result.status === 'fulfilled') {
    console.log('Success:', result.value);
  } else {
    console.log('Failed:', result.reason);
  }
});
Enter fullscreen mode Exit fullscreen mode

4. Use Async/Await in Loops — But Carefully

Looping with await can bite you if you’re not paying attention.

This is bad (sequential, slow):

async function badLoop(urls) {
  const results = [];
  for (const url of urls) {
    const res = await fetch(url); // Waits for each one
    results.push(await res.json());
  }
  return results;
}
Enter fullscreen mode Exit fullscreen mode

If you have 10 URLs, this takes ~10x the average request time.

If the requests are independent, fire them all at once:

async function goodLoop(urls) {
  const promises = urls.map(url => fetch(url).then(r => r.json()));
  return Promise.all(promises);
}
Enter fullscreen mode Exit fullscreen mode

But if you need sequential execution (e.g., rate-limited API), then await in the loop is correct:

async function rateLimitedLoop(urls) {
  const results = [];
  for (const url of urls) {
    const res = await fetch(url);
    results.push(await res.json());
    await new Promise(r => setTimeout(r, 200)); // 5 req/sec
  }
  return results;
}
Enter fullscreen mode Exit fullscreen mode

Rule of thumb: Only `await


Professional

Top comments (0)