JavaScript Event Loop & Async Programming: Promise & async/await from Theory to Practice

This article explains JavaScript's single-threaded event loop model, detailing microtask and macrotask queues, Promise chaining, async/await mechanics, common pitfalls, and practical concurrency control patterns with code examples. It covers classic output prediction exercises, Promise.all/race/allSettled/any usage, async/await error handling, loop await traps, and a reusable limited-concurrency pool implementation for batch requests.

Code Farmer Manor Chronicle
Code Farmer Manor Chronicle
Code Farmer Manor Chronicle
JavaScript Event Loop & Async Programming: Promise & async/await from Theory to Practice

Single-Threaded Event Loop Model

JavaScript is single-threaded to avoid lock complexity when multiple threads manipulate the DOM. It uses single thread + async scheduling :

JS main thread: call stack, executes synchronous code line by line
↓ When stack empties? Check queues

Event Loop:
1. Call stack empty
2. Drain microtask queue (run all)
3. Take one macrotask onto stack
4. Return to step 2

Microtasks : Promise.then/catch/finally, queueMicrotask, MutationObserver. Macrotasks : setTimeout/setInterval, setImmediate (Node), I/O callbacks, UI render events.

After each macrotask finishes, the microtask queue is completely drained before the next macrotask runs.

Microtasks effectively "cut in line" — they must finish before any subsequent macrotask.

Classic Output Exercises

Basic Example

console.log('1')
setTimeout(() => console.log('2'), 0)
Promise.resolve().then(() => {
  console.log('3')
}).then(() => {
  console.log('4')
})
console.log('5')

Execution order: 1. Synchronous code prints 1, 5. 2. setTimeout is a macrotask, queued. 3. Promise.then callbacks are microtasks, queued. 4. After sync code, microtasks run: 3, then chained 4. 5. Finally macrotask runs: 2. Output: 1 → 5 → 3 → 4 → 2 Key pitfall : setTimeout(fn, 0) does not run immediately; it runs after current macrotask and all queued microtasks complete.

Async/Await Variant

async function a() {
  console.log('a-start')
  await b()          // code after await becomes a microtask
  console.log('a-end')
}
async function b() { console.log('b') }
console.log('script-start')
a()
setTimeout(() => console.log('timeout'), 0)
Promise.resolve().then(() => console.log('promise'))
console.log('script-end')
Output: script-start → a-start → b → script-end → a-end → promise → timeout

Reason : await wraps following code into a microtask, yielding the main thread to run remaining synchronous code.

Promise Essentials

Three States & Chaining

pending ──resolve()──▶ fulfilled
↑  .then chain passes returned values forward
└-----─reject()──▶ failed ──.catch catches
fetchUser()
  .then(user => fetchOrders(user.id))  // returns new Promise, chain waits
  .then(orders => render(orders))
  .catch(err => showError(err))        // catches any prior error
  .finally(() => hideLoading())        // runs regardless

Core rules : 1. .then returning a plain value passes that value to next .then. 2. .then returning a Promise pauses chain until that Promise settles. 3. State is immutable once settled; calling resolve twice only honors first. 4. .catch can be followed by .then — error handled, chain resumes as fulfilled.

Three Combinators: Serial / Parallel / Race

// Serial: one after another (total time = sum)
for (const id of ids) { const user = await getUser(id) }

// Parallel: all start together, wait for all (time = slowest)
const [user, orders, coupons] = await Promise.all([
  getUser(id), getOrders(id), getCoupons(id)
])
// Any failure rejects entire Promise.all

// All settled (collect success/failure details):
const res = await Promise.allSettled([getUser(1), getUser(2)])
res[0].status === 'fulfilled' ? res[0].value : res[0].reason

// Race: first to settle (used for timeout)
const data = await Promise.race([
  fetchWithTimeout(url),
  new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), 5000))
])

// Any: first to fulfill (multiple CDN fallback)
const fastest = await Promise.any([cdn1Fetch(url), cdn2Fetch(url)])

Promise.all : all must succeed; one failure rejects all.

Promise.allSettled : waits for all, never rejects; inspect each result.

Promise.race : first settlement (success or failure) wins.

Promise.any : first fulfillment wins; rejects only if all fail.

Async/Await Practical Points

It's Syntactic Sugar

// Equivalent
async function load() {
  const user = await getUser()
  return user
}

function load() {
  return getUser().then(user => user)
}

Pitfall : async functions always return a Promise — return 1 actually returns Promise<1>; caller must await or .then to get 1.

Await in Loops

// ❌ map with async callback returns Promise[]
const names = users.map(async u => (await getDetail(u.id)).name)
// names is Promise[] not string[]!

// ✅ Fix with Promise.all
const names = await Promise.all(
  users.map(u => getDetail(u.id).then(d => d.name))
)

Pitfall : forEach ignores returned Promises; loop finishes before async callbacks complete.

Rule: use for...of for serial; use map + Promise.all for parallel.

Error Handling Patterns

// Pattern 1: local try-catch (fine-grained)
try { const user = await getUser() } catch (e) { return null }

// Pattern 2: top-level catch (concise but loses which step failed)
const run = async () => { /* many awaits */ }
run().catch(console.error)

Real-World: Concurrency-Limited Batch Requests

Business need: upload 200 files / fetch 1000 details — cannot Promise.all all at once (browser limits, backend overload), nor pure serial (too slow). Solution: concurrency-limited pool .

async function runWithLimit(tasks, limit = 5) {
  const results = []
  const executing = new Set()

  for (const task of tasks) {
    if (executing.size >= limit) {
      await Promise.race(executing)  // wait for any to finish
    }
    const p = Promise.resolve().then(() => task())
    executing.add(p)
    p.finally(() => executing.delete(p))
      .then(r => results.push(r))
  }
  await Promise.all(executing)  // wait remaining
  return results
}

// 200 requests, max 5 concurrent
const details = await runWithLimit(ids.map(id => () => fetchDetail(id)), 5)

Enhanced version with progress callback and order preservation:

async function runWithProgress(tasks, { limit = 5, onProgress } = {}) {
  let done = 0
  const results = []
  const executing = new Set()

  for (const [index, task] of tasks.entries()) {
    if (executing.size >= limit) await Promise.race(executing)
    const p = Promise.resolve()
      .then(() => task())
      .then(r => {
        results[index] = r   // store by original index to keep order
        done++
        onProgress?.(done, tasks.length)
      })
    executing.add(p)
    p.finally(() => executing.delete(p))
  }
  await Promise.all(executing)
  return results
}

Quick Reference Cheatsheet

Serial wait : for...of + await Parallel all : Promise.all Parallel fault-tolerant : Promise.allSettled Timeout control : Promise.race([request, timeoutPromise]) Multi-source race : Promise.any Limit concurrency : pool pattern + Promise.race for slot replenishment

Microtasks : .then callbacks, queueMicrotask Macrotasks : setTimeout, I/O, event callbacks

Takeaway : The event loop is the dividing line between superficial and deep JavaScript understanding. Internalize the two-phase model and "macrotasks must wait for microtask queue to empty" — then output-order questions become trivial.

Further reading: high-frequency events (scroll/resize/input) from the event loop; see companion article on debounce and throttle.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaScriptConcurrency Controlasync/awaitEvent LoopPromiseMicrotasksMacrotasksPromise.all
Code Farmer Manor Chronicle
Written by

Code Farmer Manor Chronicle

A heart like drifting clouds, ever at ease; a mind like flowing water, free to roam.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.