๐Ÿ” Day 22 — Promises & Async/Await Deep Dive

๐Ÿ” Day 22 — Promises & Async/Await Deep Dive

Hey Guys, Asynchronous JavaScript is essential for network calls, timers, and background work. Today we’ll go deep — Promises internals, chaining patterns, async/await, concurrency, error handling, and useful utility wrappers.


๐Ÿ“Œ Recap: What is a Promise?

A Promise represents an eventual value: a pending computation that either fulfills with a value or rejects with an error. It’s a replacement for callback hell and enables clearer flow control.

⚙ Creating and using Promises

const p = new Promise((resolve, reject) => {
  // async work
  setTimeout(() => resolve('done'), 1000);
});

p.then(value => console.log(value)).catch(err => console.error(err));
  

๐Ÿ”— Chaining & returning promises

Returning a promise inside .then() chains it. This makes sequential async flows readable.

fetch('/users')
  .then(res => res.json())
  .then(users => fetch(`/posts?user=${users[0].id}`))
  .then(res => res.json())
  .then(posts => console.log(posts))
  .catch(err => console.error('Chain error:', err));
  

⚡ async / await — syntactic sugar

async functions return a promise. Use await to pause until a promise resolves — which leads to code that reads top-to-bottom.

async function loadUserPosts() {
  try {
    const r1 = await fetch('/users');
    const users = await r1.json();
    const r2 = await fetch(`/posts?user=${users[0].id}`);
    const posts = await r2.json();
    console.log(posts);
  } catch (err) {
    console.error('Async error:', err);
  }
}
loadUserPosts();
  

๐Ÿง  Parallel vs Sequential

Use Promise.all for parallel requests. Avoid awaiting sequentially when calls are independent.

// sequential (slower)
await fetch('/a');
await fetch('/b');

// parallel (faster)
const [aRes, bRes] = await Promise.all([fetch('/a'), fetch('/b')]);
const [a, b] = await Promise.all([aRes.json(), bRes.json()]);
  

๐Ÿ” Useful patterns

1) Safe wrapper (to handle errors without try/catch everywhere)

const to = p => p.then(data => [null, data]).catch(err => [err, null]);

const [err, data] = await to(fetch('/api').then(r => r.json()));
if (err) {
  console.error(err);
} else {
  console.log(data);
}
  

2) Retry with backoff

async function retry(fn, attempts = 3, delay = 500) {
  for (let i = 0; i < attempts; i++) {
    try {
      return await fn();
    } catch (e) {
      if (i === attempts - 1) throw e;
      await new Promise(r => setTimeout(r, delay * Math.pow(2, i)));
    }
  }
}
  

3) Timeout a promise

function withTimeout(p, ms) {
  const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), ms));
  return Promise.race([p, timeout]);
}

await withTimeout(fetch('/slow'), 3000);
  

๐Ÿงช Error handling strategies

  • Handle errors close to where recovery is possible; otherwise bubble up.
  • Use centralized logging for production and friendly UI messages for users.
  • For long chains, use the safe wrapper (to()) or a top-level try/catch in async functions.

๐Ÿ”ง Advanced concurrency controls

When you need to limit concurrent requests (e.g., rate limits), implement a simple queue / pool.

// simple concurrency limiter (pool size = 3)
async function pool(tasks, size = 3) {
  const results = [];
  const executing = [];
  for (const task of tasks) {
    const p = Promise.resolve().then(() => task());
    results.push(p);
    const e = p.then(() => executing.splice(executing.indexOf(e), 1));
    executing.push(e);
    if (executing.length >= size) await Promise.race(executing);
  }
  return Promise.all(results);
}
  

๐Ÿ“ Practice tasks

  1. Refactor a chain of fetch calls into async/await and add a retry wrapper.
  2. Implement a safeFetch wrapper that returns [err,data].
  3. Build a small uploader that uploads 5 files but only 2 at a time (use pool).

๐ŸŽฏ Summary

Promises and async/await are essential for modern JavaScript. Learn to compose promises, handle errors, and control concurrency — these patterns turn brittle code into reliable systems.

Comments