NodeJSasync / await in Node.js

async / await in Node.js

async/await is syntax sugar over promises that lets you write asynchronous code that reads like synchronous code — top to bottom, with ordinary try/catch and loops. It's the modern default for async work in Node. Crucially, it doesn't replace promises; it's a nicer way to consume them, so everything you know about promises still applies underneath.

The two keywords

Keyword

Does

async

Marks a function; it now always returns a promise

await

Pauses the async function until a promise settles, yielding its value

JS
async function getUserName(id) {
  const user = await fetchUser(id)   // pause here until the promise settles
  return user.name                    // returned value becomes the promise's result
}

getUserName(1).then((name) => console.log(name))   // it's still a promise outside
`async` functions always return a promise
Whatever you `return` from an async function is wrapped in a fulfilled promise; whatever you `throw` becomes a rejected one. So `getUserName(1)` above is a promise even though the body `return`s a string. This is why you still `.then`/`await` the *call*.
Error handling with try/catch

await turns a rejected promise into a thrown error — so the familiar try/catch you'd use for synchronous code now covers async failures too:

JS
async function loadProfile(id) {
  try {
    const user = await fetchUser(id)
    const posts = await fetchPosts(user.id)
    return { user, posts }
  } catch (err) {
    console.error('load failed:', err.message)   // catches EITHER await's rejection
    throw err                                      // re-throw to let the caller decide
  } finally {
    console.log('done')                            // always runs
  }
}
A bare `await` with no try/catch can crash the process
If an awaited promise rejects and nothing catches it, the async function's returned promise rejects — and if *that's* unhandled, modern Node terminates with an `unhandledRejection`. Either wrap awaits in `try/catch`, or ensure the caller handles the returned promise's rejection. Never `await` at the top of a request handler without a safety net.
The sequential trap
Awaiting independent operations in series wastes time
This is the most common `async/await` mistake. Each `await` *pauses* until that promise settles — so awaiting three independent operations one after another makes them run **sequentially** when they could run **concurrently**. The total time becomes the *sum* instead of the *max*.

JS
// ✗ SLOW — 300ms total (100 + 100 + 100), they don't depend on each other
const a = await getA()   // wait 100ms
const b = await getB()   // then wait 100ms
const c = await getC()   // then wait 100ms

// ✓ FAST — ~100ms total, all three run at once
const [a2, b2, c2] = await Promise.all([getA(), getB(), getC()])
Sequential is correct when steps DEPEND on each other
Use sequential `await` when a later step needs an earlier step's result (`const user = await getUser(); const posts = await getPosts(user.id)`). Use `Promise.all` when steps are independent. The skill is recognizing which is which — start each operation as early as its inputs allow.
await in loops

for…of with await runs iterations one at a time — correct for ordered/dependent work, but slow for independent items. For concurrency, map to promises and Promise.all:

JS
// Sequential — one request at a time (use when order/rate matters)
for (const id of ids) {
  const user = await fetchUser(id)
  console.log(user.name)
}

// Concurrent — all at once (use for independent items)
const users = await Promise.all(ids.map((id) => fetchUser(id)))
`forEach` does NOT await — it silently ignores async
`ids.forEach(async (id) => { await fetchUser(id) })` does **not** wait — `forEach` ignores the returned promises, so the surrounding code races ahead and rejections go unhandled. Use a `for…of` loop (sequential) or `Promise.all(map(...))` (concurrent). Never `async` inside `forEach`/`map` expecting it to wait.
Top-level await

In ES modules (and the REPL), you can await at the top level — no wrapping async function needed:

Only in ESM (.mjs or "type": "module")

JS
import { readFile } from 'node:fs/promises'

const config = JSON.parse(await readFile('config.json', 'utf8'))
console.log(config.port)
Top-level await is ESM-only
This works in ES modules but not in CommonJS, where the top level is synchronous. If you need it in a CJS file, either convert to ESM or wrap in an `async` IIFE. The module-system differences are covered in [CommonJS vs ES Modules](/nodejs/commonjs-vs-esm).
Concurrency control for large batches
`Promise.all` over thousands of items can overwhelm
Mapping 10,000 items straight into `Promise.all` fires 10,000 simultaneous requests — exhausting sockets, memory, or hitting rate limits. For large batches, cap concurrency: process in chunks, or use a limiter (e.g. `p-limit`) so only N run at once. Patterns for this are in [Async Control-Flow Patterns](/nodejs/async-patterns).
Next
Bridge the remaining callback APIs into this world: [util.promisify](/nodejs/promisify).