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.
On this page · 6 min · Intermediate
On this page
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 6 min
- Difficulty
- Intermediate
- Updated
- Oct 28, 2026
Related tutorials
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#
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#
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);
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#
- saveProduct is async: it awaits a delay and throws for invalid data, which rejects its promise.
- importSequential uses for...of with await and catches each error so one bad row does not stop the import.
- importParallel maps rows to promises and uses allSettled to collect all outcomes at once.
- Top-level await works because the file is an ES module.
- 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#
const user = getUser(id); user.name is undefined because user is a promise.
array.forEach(async item => await save(item)) does not wait; the function returns immediately. Use for...of or Promise.all.
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?
Show solution
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#
What does an async function return?
Want more? Practise all Node.js MCQs · Node.js interview questions
My notes#
Saved in this browser.