NoNode.js · Lesson 5 of 7

Async Patterns

Node.js is built around async I/O. Understanding how the event loop works — and the patterns that emerge from it — is the key to writing fast, non-blocking Node code.

JavaScript
// The callback era (still exists in legacy code)
import { readFile } from 'fs'

readFile("data.txt", "utf-8", (err, data) => {
  if (err) { console.error(err); return }
  console.log(data)
})

// Promises — cleaner than callbacks
import { readFile as readFilePromise } from 'fs/promises'

readFilePromise("data.txt", "utf-8")
  .then(data => console.log(data))
  .catch(err => console.error(err))

// async/await — cleanest
async function readAndLog(path) {
  try {
    const data = await readFilePromise(path, "utf-8")
    console.log(data)
  } catch (err) {
    console.error("Failed:", err.message)
  }
}

// Promise.all — run multiple async operations in parallel
async function readMultiple(paths) {
  const contents = await Promise.all(paths.map(p => readFilePromise(p, "utf-8")))
  return contents
}

// Promise.allSettled — don't fail fast on errors
async function readSafe(paths) {
  const results = await Promise.allSettled(paths.map(p => readFilePromise(p, "utf-8")))
  return results.map((r, i) => ({
    path: paths[i],
    content: r.status === "fulfilled" ? r.value : null,
    error: r.status === "rejected" ? r.reason.message : null,
  }))
}
JavaScript
// Concurrency control — don't fire 10,000 requests at once
async function processInBatches(items, batchSize, asyncFn) {
  const results = []
  for (let i = 0; i < items.length; i += batchSize) {
    const batch = items.slice(i, i + batchSize)
    const batchResults = await Promise.all(batch.map(asyncFn))
    results.push(...batchResults)
    console.log(`Processed ${Math.min(i + batchSize, items.length)}/${items.length}`)
  }
  return results
}

// Timeout wrapper
function withTimeout(promise, ms, message = "Timeout") {
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error(message)), ms)
  )
  return Promise.race([promise, timeout])
}

// Retry with exponential backoff
async function retry(fn, { attempts = 3, baseDelay = 100 } = {}) {
  let lastError
  for (let i = 0; i < attempts; i++) {
    try {
      return await fn()
    } catch (err) {
      lastError = err
      if (i < attempts - 1) {
        const delay = baseDelay * 2 ** i + Math.random() * 100
        await new Promise(r => setTimeout(r, delay))
      }
    }
  }
  throw lastError
}

// Usage
const data = await retry(
  () => fetch("https://api.example.com/data").then(r => r.json()),
  { attempts: 3, baseDelay: 200 }
)
✦ Tip
Node.js is single-threaded but non-blocking. CPU-intensive work (image processing, encryption, parsing) blocks the event loop. Move it to `worker_threads` or a child process. Use `--prof` or the built-in `node --inspect` + Chrome DevTools to profile before optimizing.