Programming Fundamentals

Asynchronous Programming

3 min read
Focus: PROGRAMMING-FUNDAMENTALS

TL;DR — Quick Summary

  • Use async/await with try/catch for readable async code — it's synchronous-looking but non-blocking.
  • Use Promise.all() to run independent async operations in parallel — not sequentially with multiple await calls.
  • Use Promise.allSettled() when you want all results even if some fail — Promise.all() rejects immediately on first failure.
  • Never use await in a regular for loop over independent operations — use Promise.all(arr.map(fn)) instead.

Lesson Overview

JavaScript runs on a single thread — it can only do one thing at a time. But web applications constantly need to do things that take time: fetching data from an API, reading files, waiting for user input. Asynchronous programming is how JavaScript handles these operations without blocking the main thread.

The evolution of async JavaScript: callbacks (original, error-prone) → Promises (chainable, better error handling) → async/await (syntactic sugar over Promises, reads like synchronous code). Modern code uses async/await almost exclusively, but understanding Promises is essential because all async operations return them under the hood.

Conceptual Deep Dive

The Event Loop: JavaScript uses an event loop. Synchronous code runs first (call stack). Async callbacks/Promise handlers queue up and run after the call stack is empty. This is why console.log after a setTimeout(fn, 0) still runs before the timeout callback.

Promises: Represent a future value — in one of three states: pending, fulfilled, or rejected. Key methods:

  • .then(onFulfilled): handle success
  • .catch(onRejected): handle failure
  • .finally(fn): always runs
  • Promise.all(arr): wait for all — rejects if any fail
  • Promise.allSettled(arr): wait for all — gets all results regardless
  • Promise.race(arr): resolves/rejects with the first to finish
  • Promise.any(arr): resolves with the first to succeed

async/await: async makes a function return a Promise. await pauses execution until the Promise resolves. Must be inside an async function. Error handling uses standard try/catch.

Implementation Lab

Promises and async/await
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// Promise: represents a value available in the future
const fetchUser = (id) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id > 0) {
        resolve({ id, name: 'Alice', email: 'alice@example.com' });
      } else {
        reject(new Error('Invalid user ID'ID'));
      }
    }, 1000);
  });
};
 
// Promise chain
fetchUser(1)
  .then(user => {
    console.log('User:', user.name);
    return fetchUser(2); // return next promise
  })
  .then(user2 => console.log('User2:', user2.name))
  .catch(err => console.error('Error:', err.message))
  .finally(() => console.log('Done'));
 
// ✅ async/await — same result, cleaner syntaxawait — same result, cleaner syntax
async function loadUsers() {
  try {
    const user1 = await fetchUser(1);
    console.log('User:', user1.name);
 
    const user2 = await fetchUser(2);
    console.log('User2:', user2.name);
  } catch (err) {
    console.error('Error:', err.message);
  } finally {
    console.log('Done');
  }
}
Parallel vs Sequential Requests
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// ❌ Sequential — waits for each to complete before starting nextfor each to complete before starting next
async function loadSequential() {
  const user = await fetchUser(1);     // wait 1s
  const posts = await fetchPosts(1);   // wait another 1s
  const comments = await fetchComments(1); // wait another 1s
  // Total: ~3 seconds3 seconds
  return { user, posts, comments };
}
 
// ✅ Parallel — start all at once, wait for all to completefor all to complete
async function loadParallel() {
  const [user, posts, comments] = await Promise.all([
    fetchUser(1),
    fetchPosts(1),
    fetchComments(1)
  ]);
  // Total: ~1 second (all run simultaneously)1 second (all run simultaneously)
  return { user, posts, comments };
}
 
// Promise.allSettled — get all results, even if some failif some fail
async function loadWithFallback() {
  const results = await Promise.allSettled([
    fetchUser(1),
    fetchUser(-1),  // This will fail
    fetchUser(3)
  ]);
 
  results.forEach((result, i) => {
    if (result.status === 'fulfilled') {
      console.log(`User ${i}:`{i}:`, result.value.name);
    } else {
      console.error(`User ${i} failed:`{i} failed:`, result.reason.message);
    }
  });
}
 
// Real-world: fetch with error handling
async function getProduct(id) {
  const response = await fetch(`/api/products/${id}`}`);
 
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`{response.status}`);
  }
 
  const data = await response.json();
  return data;
}

Pro Tips — Senior Dev Insights

1

Create a reusable fetchJSON(url) wrapper that handles response.ok checking and JSON parsing — centralise your fetch error handling instead of repeating it everywhere.

2

For retrying failed requests: use a recursive async function with a delay — libraries like p-retry do this well, but a simple version takes 10 lines.

3

The async IIFE pattern lets you use await at the top level in older environments: (async () => { const data = await fetch(...); })().

Common Developer Pitfalls

!
Sequential awaits for independent operations — await a(); await b(); when a and b don't depend on each other. Use Promise.all([a(), b()]) to run in parallel.
!
Forgetting that await only works inside async functions — using it at the top level of a module requires 'top-level await' (supported in modern environments).
!
Not handling rejected promises — unhandled rejections cause warnings in browsers and crashes in Node.js. Always attach .catch() or use try/catch with await.
!
Using async on functions that don't need it — unnecessary async wrapping adds micro-task overhead and misleads readers about the function's nature.

Interview Mastery

JavaScript is single-threaded — it executes one operation at a time. The event loop continuously checks if the call stack is empty, then moves items from the task queue to the stack. Async operations (setTimeout, fetch, Promises) are offloaded to browser APIs/Node.js internals. When they complete, their callbacks are queued. The event loop picks them up when the call stack is clear. This is why JavaScript can be non-blocking despite being single-threaded — async operations don't sit on the call stack waiting; they're handled elsewhere and their results are queued.

Promise.all takes an array of promises and resolves with all their values when all succeed — but rejects immediately if any one fails. Use it when all results are required and a single failure means the operation should fail. Promise.allSettled waits for all promises to complete regardless of success or failure, returning an array of { status: 'fulfilled'/'rejected', value/reason } objects. Use it when you want all results, even partial successes — for example, loading multiple optional dashboard widgets where some failing shouldn't block others.

async/await is syntactic sugar over Promises — under the hood, async functions return Promises and await pauses execution inside the async function until the Promise resolves. async/await reads like synchronous code (top-to-bottom), handles errors with familiar try/catch, and is easier to debug (stack traces are cleaner). Promise chains use .then()/.catch() which are fluent but can become nested ('promise hell') with complex conditional logic. Both approaches are equivalent in capability — async/await is strongly preferred for readability in modern code.

Hands-on Lab Exercises

1

Build a data fetching function using async/await that: fetches from an API, checks response.ok, parses JSON, handles errors gracefully, and shows/hides a loading state.

2

Convert a Promise chain (.then().then().catch()) to async/await — verify both produce identical results.

3
Build a parallel data loader using Promise.all — time it against sequential awaits and compare the duration.
4
Implement a simple retry(fn, retries, delay) async function that retries a failing async function up to N times with a delay between attempts.

Real-World Practice Scenarios

Dashboard loader: Load user data, recent activity, and notifications in parallel with Promise.all. Show each section as it loads using Promise.allSettled.
Form submission: Async form handler — validate, submit, handle success/failure, show loading state, and use finally to always re-enable the submit button.
Search autocomplete: Debounced async search — cancel the previous request if a new one starts before it completes (using AbortController).
File upload: Upload multiple files in parallel, track progress per file, report which succeeded and which failed using Promise.allSettled.

Deepen Your Knowledge