NodeJSasync / await Refresher

async / await Refresher

async/await is syntactic sugar over promises that lets you write asynchronous code that reads like synchronous code — top to bottom, with normal try/catch error handling. It is the dominant style in modern Node.js. Critically, it does not replace promises — every async function returns one, and await only knows how to wait on them.

The two keywords
  • async before a function makes it always return a promise — a returned value is auto-wrapped, a thrown error becomes a rejection.

  • await pauses only the current async function until a promise settles, then yields its value (or throws its rejection). The event loop keeps serving everyone else meanwhile.

JS
async function getUser(id) {
  const res = await fetch(`/api/users/${id}`)  // pause until response
  const user = await res.json()                 // pause until parsed
  return user                                    // wrapped in a promise
}

const user = await getUser(1)
await yields the thread, it does not block it
This is the whole point. While one request is parked at `await fetch(...)`, Node is free to run other requests, timers, and I/O on the same thread. `await` suspends *this function*; it does **not** freeze the process the way a synchronous loop would. See [the single-threaded model](/nodejs/single-threaded-model).
Error handling with try/catch

A rejected awaited promise throws at the await, so you handle it with ordinary try/catch/finally — the thing callbacks could never do:

JS
async function loadConfig() {
  try {
    const data = await readFile('config.json', 'utf8')
    return JSON.parse(data)
  } catch (err) {
    console.error('Could not load config:', err.message)
    return {}                 // sensible fallback
  } finally {
    console.log('load attempt finished')
  }
}
An un-awaited promise swallows its error
If you call an async function without `await` (or a `.catch`), a rejection becomes an *unhandled rejection* that try/catch cannot see — because control already moved on. Either `await` it, attach `.catch()`, or deliberately fire-and-forget with `void doThing().catch(log)`. A bare `doThing()` is a latent crash.
Sequential vs parallel — a costly mistake
await in a loop serializes everything
Awaiting one-by-one when the operations are independent makes them run **sequentially**, multiplying total latency. Fire them off together and await the group with `Promise.all`.

Slow — 3x the latency (each waits for the previous)

JS
const a = await fetchA()   // wait 100ms...
const b = await fetchB()   // then wait 100ms...
const c = await fetchC()   // then wait 100ms...  → ~300ms total

Fast — all in flight at once

JS
const [a, b, c] = await Promise.all([fetchA(), fetchB(), fetchC()])
// → ~100ms total
Kick off, then await
The key insight: `Promise.all([fetchA(), fetchB()])` works because the functions are *called* (started) immediately when the array is built — only the `await` waits. If each step *depends* on the previous result, sequential `await` is correct. Parallelize only **independent** work.
Iterating async work properly

JS
// Parallel — all at once (watch out for rate limits / memory)
await Promise.all(ids.map((id) => processItem(id)))

// Strictly in order — when each must finish before the next
for (const id of ids) {
  await processItem(id)
}
map(async) without Promise.all does nothing useful
`ids.map(async (id) => await processItem(id))` returns an **array of pending promises** that you have not waited for — the loop appears to "finish" instantly while work runs unsupervised. Always wrap it in `await Promise.all(...)`. For *bounded* concurrency (e.g. 5 at a time), use a pool library like `p-limit` instead of unleashing thousands at once.
Top-level await

In ES modules you can await at the top level of a file without an async wrapper — handy for setup and configuration. It is not available in CommonJS:

setup.mjs

JS
const config = await loadConfig()
const db = await connectToDatabase(config.dbUrl)
export { db }
Top-level await delays importers
A module that uses top-level await does not finish loading until that await settles — and any module that imports it waits too. Great for one-time initialization; avoid long or flaky awaits at module scope, or you stall your whole startup.
How it maps to promises

async/await

Promise equivalent

return value

Promise.resolve(value)

throw err

Promise.reject(err)

const x = await p

p.then((x) => ...)

try/catch

.catch(...)

finally

.finally(...)

Next
You are fluent in async. Round out the refresher with [ES6+ Features You Need](/nodejs/es6-features), then start the [Node.js Architecture Overview](/nodejs/architecture).