DEV Community

Alex Chen
Alex Chen

Posted on

Async/Await in JavaScript: From Callbacks to Clean Code

Async/Await in JavaScript: From Callbacks to Clean Code

JavaScript's evolution from callback hell to clean async code.

The Evolution

Stage 1: Callbacks (The Dark Ages)

// Callback hell: nested, hard to read, error-prone
getUser(id, (user) => {
  getPosts(user.id, (posts) => {
    getComments(posts[0].id, (comments) => {
      getAuthor(comments[0].authorId, (author) => {
        console.log(author.name);
        // Error handling? Good luck.
      });
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

Stage 2: Promises (Better)

// Flat chain but still verbose
getUser(id)
  .then(user => getPosts(user.id))
  .then(posts => getComments(posts[0].id))
  .then(comments => getAuthor(comments[0].authorId))
  .then(author => console.log(author.name))
  .catch(err => console.error('Something failed:', err));
Enter fullscreen mode Exit fullscreen mode

Stage 3: Async/Await (Modern)

// Reads like synchronous code!
async function getAuthorName(id) {
  try {
    const user = await getUser(id);
    const posts = await getPosts(user.id);
    const comments = await getComments(posts[0].id);
    const author = await getAuthor(comments[0].authorId);
    return author.name;
  } catch (err) {
    console.error('Failed:', err);
    throw err;
  }
}
Enter fullscreen mode Exit fullscreen mode

How Async/Await Works

Code execution:

async function example() {
  console.log('1');                    // Runs immediately
  const data = await fetchData();      // PAUSES here, returns to event loop
  console.log('3');                    // Runs AFTER data arrives
}
console.log('2');                      // Runs while waiting for fetchData

// Output: 1, 2, 3

Timeline:
  ──── 1 ──── await fetchData() ────── 3 ────
                                  
      (paused)               (resumed when promise resolves)
         ──── 2 ──── (runs during the wait)
Enter fullscreen mode Exit fullscreen mode

Core Patterns

Pattern 1: Sequential Execution

// One after another (slow — each waits for the previous)
async function processUsers() {
  const user1 = await getUser(1); // 1 second
  const user2 = await getUser(2); // 1 second
  const user3 = await getUser(3); // 1 second
  // Total: 3 seconds
}
Enter fullscreen mode Exit fullscreen mode

Pattern 2: Parallel Execution

// All at once (fast!)
async function processUsers() {
  const [user1, user2, user3] = await Promise.all([
    getUser(1),
    getUser(2),
    getUser(3),
  ]);
  // Total: 1 second (all run concurrently)
}
Enter fullscreen mode Exit fullscreen mode

Pattern 3: Parallel with Error Handling

// Get results that succeeded, collect errors
async function fetchMultiple(urls) {
  const results = await Promise.allSettled(
    urls.map(url => fetch(url).then(r => r.json()))
  );

  const succeeded = results.filter(r => r.status === 'fulfilled').map(r => r.value);
  const failed = results.filter(r => r.status === 'rejected').map(r => r.reason);

  console.log(`${succeeded.length} succeeded, ${failed.length} failed`);
  return succeeded;
}
Enter fullscreen mode Exit fullscreen mode

Pattern 4: Timeout Wrapper

// Reject promise if it takes too long
function withTimeout(promise, ms) {
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms)
  );
  return Promise.race([promise, timeout]);
}

// Usage
const data = await withTimeout(fetch('/api/data'), 5000);
// If fetch takes > 5 seconds, throws "Timeout after 5000ms"
Enter fullscreen mode Exit fullscreen mode

Pattern 5: Retry with Backoff

async function fetchWithRetry(url, retries = 3, delay = 1000) {
  for (let i = 0; i < retries; i++) {
    try {
      const response = await fetch(url);
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      return await response.json();
    } catch (err) {
      if (i === retries - 1) throw err;
      console.warn(`Attempt ${i + 1} failed, retrying in ${delay}ms...`);
      await new Promise(resolve => setTimeout(resolve, delay));
      delay *= 2; // Exponential backoff
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Pattern 6: Async Iteration

// Process items one at a time (don't overwhelm the server)
async function processItems(items) {
  for (const item of items) {
    await processItem(item); // Wait for each to complete before next
  }
}

// Process in batches
async function processInBatches(items, batchSize = 5) {
  for (let i = 0; i < items.length; i += batchSize) {
    const batch = items.slice(i, i + batchSize);
    await Promise.all(batch.map(processItem));
    console.log(`Processed batch ${Math.floor(i / batchSize) + 1}`);
  }
}
Enter fullscreen mode Exit fullscreen mode

Common Mistakes

❌ Forgetting await

async function getData() {
  const data = fetchData(); // Missing await!
  console.log(data);         // Promise object, not the data!
}

// ✅ Fix: Always await
async function getData() {
  const data = await fetchData();
  console.log(data); // The actual data
}
Enter fullscreen mode Exit fullscreen mode

❌ Using await in loops unnecessarily

// Slow: Sequential
for (const id of [1, 2, 3, 4, 5]) {
  await fetchUser(id); // Each waits for the previous
}

// Fast: Parallel
await Promise.all([1, 2, 3, 4, 5].map(id => fetchUser(id)));
Enter fullscreen mode Exit fullscreen mode

❌ Forgetting try/catch

// Unhandled rejection crashes Node.js!
async function risky() {
  const data = await mightFail(); // No try/catch
}
Enter fullscreen mode Exit fullscreen mode

❌ await in constructor

// You can't use await in constructors!
class MyClass {
  constructor() {
    this.data = await loadData(); // SyntaxError!
  }
}

// ✅ Fix: Use static factory method
class MyClass {
  data: any;
  private constructor(data) { this.data = data; }

  static async create() {
    const data = await loadData();
    return new MyClass(data);
  }
}

const instance = await MyClass.create();
Enter fullscreen mode Exit fullscreen mode

Promise Utilities

// Promise.all() — All must succeed
const [a, b, c] = await Promise.all([p1, p2, p3]);
// If ANY fails → throws immediately

// Promise.allSettled() — Wait for ALL (success or fail)
const results = await Promise.allSettled([p1, p2, p3]);
// Always returns array of { status, value/reason }

// Promise.race() — First to resolve OR reject
const winner = await Promise.race([slowFetch, fastFetch]);
// Returns as soon as one completes

// Promise.any() — First to succeed
const firstSuccess = await Promise.any([
  fetch(url1).catch(() => {}),
  fetch(url2).catch(() => {}),
]);
// Returns first successful result, ignores failures
Enter fullscreen mode Exit fullscreen mode

Quick Reference

Pattern Code
Sequential const a = await p1; const b = await p2;
Parallel const [a, b] = await Promise.all([p1, p2]);
Error handling try { await p } catch (e) {}
Timeout Promise.race([p, timeout(5000)])
Retry Loop with try/catch and delay
Map+parallel await Promise.all(items.map(fn))

What async pattern do you use most? Any I missed?

Follow @armorbreak for more JavaScript content.

Top comments (0)