Node.js · Async Patterns

Async and Await

async/await makes asynchronous code look synchronous: you await a result and continue on the next line. It is the standard way to write Node.js code today.

Intermediate 6 min Lesson 17 of 37Examples target Node.js 22 LTS.
Before you start:Promises
0/37 in Node.js
On this page · 6 min · Intermediate

Learning objectives#

  • Write async functions and await promises
  • Handle errors with try/catch/finally
  • Loop over async work sequentially or in parallel
  • Use top-level await in ES modules

Explanation#

An async function always returns a promise. Inside it, await pauses that function (not the whole program) until the promise settles.

Use try/catch around awaits to handle rejections, and finally for cleanup.

for...of with await processes items one by one (safe for rate limits or ordering). For independent items, map to promises and await Promise.all.

ES modules support top-level await — useful for scripts and for loading config before starting a server.

Do not forget await: calling an async function without it gives you a promise, and errors become unhandled rejections.

Syntax#

JavaScript Illustrative
async function loadDashboard(userId) {
  try {
    const user = await getUser(userId);
    const orders = await getOrders(user.id);
    return { user, orders };
  } finally {
    await db.release();
  }
}

Code example: Importing a list of products#

JavaScriptimport.js Illustrative
const wait = ms => new Promise(r => setTimeout(r, ms));


// Pretend database call
async function saveProduct(product) {
  await wait(20);
  if (product.price <= 0) throw new Error(`Invalid price for ${product.name}`);
  return { ...product, id: Math.round(product.price * 7) };
}


const rows = [
  { name: 'Pen', price: 20 },
  { name: 'Broken item', price: 0 },
  { name: 'Notebook', price: 60 },
];


// Sequential: one at a time, keep going on errors
async function importSequential(items) {
  const saved = [];
  const failed = [];
  for (const item of items) {
    try {
      saved.push(await saveProduct(item));
    } catch (err) {
      failed.push(err.message);
    }
  }
  return { saved, failed };
}


// Parallel: all at once, collect every outcome
async function importParallel(items) {
  const results = await Promise.allSettled(items.map(saveProduct));
  return {
    saved: results.filter(r => r.status === 'fulfilled').map(r => r.value),
    failed: results.filter(r => r.status === 'rejected').map(r => r.reason.message),
  };
}


const seq = await importSequential(rows);           // top-level await (ES module)
console.log('Sequential saved:', seq.saved.map(p => `${p.name}#${p.id}`).join(', '));
console.log('Sequential failed:', seq.failed);


const par = await importParallel(rows);
console.log('Parallel saved:', par.saved.length, '| failed:', par.failed.length);


const forgotAwait = saveProduct(rows[0]);
console.log('Without await you get:', forgotAwait instanceof Promise ? 'a Promise' : forgotAwait);
Expected output
Sequential saved: Pen#140, Notebook#420
Sequential failed: [ 'Invalid price for Broken item' ]
Parallel saved: 2 | failed: 1
Without await you get: a Promise

This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.

Step-by-step explanation#

  1. saveProduct is async: it awaits a delay and throws for invalid data, which rejects its promise.
  2. importSequential uses for...of with await and catches each error so one bad row does not stop the import.
  3. importParallel maps rows to promises and uses allSettled to collect all outcomes at once.
  4. Top-level await works because the file is an ES module.
  5. Calling an async function without await returns a promise, not the value.

Real-world use case#

A nightly job imports 5,000 products from a supplier CSV: it processes them in batches of 50 with Promise.allSettled, logs failed rows, and finishes in minutes instead of an hour.

Common mistakes#

Missing await

const user = getUser(id); user.name is undefined because user is a promise.

await in forEach

array.forEach(async item => await save(item)) does not wait; the function returns immediately. Use for...of or Promise.all.

One giant try/catch

Catching everything at the top hides which step failed. Catch where you can add context or recover.

Best practices#

  • Await every promise you depend on
  • for...of for sequential work, Promise.all for parallel
  • try/catch close to the operation that can fail
  • Use top-level await only in entry modules

Remember this#

async function → returns a promise.

await pauses this function only.

Never await inside forEach — use for...of or Promise.all.

Practice exercise#

Your task: Write an async function retry(fn, times) that calls fn up to `times` times, returning the first success or throwing the last error.

Need a hint?
A for loop with try { return await fn(); } catch (err) { last = err; }.
Show solution
JavaScript Illustrative
async function retry(fn, times) {
  let lastError;
  for (let attempt = 1; attempt <= times; attempt++) {
    try {
      return await fn(attempt);
    } catch (err) {
      lastError = err;
    }
  }
  throw lastError;
}


const flaky = async attempt => {
  if (attempt < 3) throw new Error(`attempt ${attempt} failed`);
  return 'connected';
};
console.log(await retry(flaky, 5));   // connected

Mini quiz#

Async and Await quizMultiple choice Question 1 of 3

What does an async function return?

Score so far: 0/0

Want more? Practise all Node.js MCQs · Node.js interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.