Node.js: Promises and Async/Await

Last updated: 2026-08-26

1. What You'll Learn



2. Story: Charlie's Bulk Data Download

Charlie was responsible for downloading configuration data from 100 external APIs. At first, he used for to make requests one by one in a loop; each request took about 30 seconds, so it took 50 minutes to complete all 100. Later, he switched to Promise.all to make parallel requests, reducing the total time to 30 seconds—but the server immediately returned 429 Too Many Requests. Charlie realized he needed to limit the value of concurrent requests and ultimately capped it at 5, bringing the total time down to about 10 minutes—a solution that was both efficient and didn’t trigger rate limiting.

This story highlights the core trade-off in asynchronous programming: speed vs. resources. Promises and async/await are the tools for managing this balance.



3. fs.promises API

Starting with version 10, Node.js provides fs.promises, which wraps file system operations into methods that return Promises, putting an end to callback hell.

(1) fs callbacks vs fs.promises vs promisify

Feature fs callbacks fs.promises util.promisify(fs.xxx)
Return value void; the result is passed via a callback Promise Promise
Error Handling The first parameter of the callback, err try/catch or .catch() try/catch or .catch()
Code Style Nested Callbacks Flat async/await Flat async/await
Available Versions All Versions v10+ v8+
Typical Usage fs.readFile(path, (err, data) => {}) await fs.promises.readFile(path) const readFile = promisify(fs.readFile); await readFile(path)

▶ Example: Reading and Writing Files with fs.promises

JAVASCRIPT
const fs = require('fs').promises;

async function readAndWrite() {
  try {
    const data = await fs.readFile('input.txt', 'utf8');
    const upper = data.toUpperCase();
    await fs.writeFile('output.txt', upper);
    console.log('Done');
  } catch (err) {
    console.error('Error:', err.message);
  }
}

readAndWrite();
▶ Try it Yourself

▶ Example: Directory operations with fs.promises

JAVASCRIPT
const fs = require('fs').promises;

async function listFiles(dir) {
  try {
    await fs.mkdir(dir, { recursive: true });
    const files = await fs.readdir(dir);
    for (const file of files) {
      const stat = await fs.stat(`${dir}/${file}`);
      console.log(`${file} - ${stat.size} bytes`);
    }
  } catch (err) {
    console.error(err.message);
  }
}

listFiles('./my-dir');
▶ Try it Yourself

▶ Example: util.promisify converts callback functions

JAVASCRIPT
const fs = require('fs');
const { promisify } = require('util');

const readFile = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile);

async function copy() {
  const data = await readFile('source.txt', 'utf8');
  await writeFile('dest.txt', data);
  console.log('Copied');
}

copy();
▶ Try it Yourself

4. Promise Chaining Methods

Four static methods determine how multiple Promises interact with each other; choosing the wrong combination of methods can lead to vastly different results.

(1) Comparison of Four Combination Methods

Method When all operations succeed When there is a failure Return Value Typical Uses
Promise.all Return the entire result array Reject upon the first failure; reject the entire set Result array All tasks must be completed
Promise.race Return the first completed result Reject upon first failure Single value Timeout control, fastest response
Promise.allSettled Return all results Do not reject; include failure messages {status, value/reason}[] Return all results, regardless of success or failure
Promise.any Return the first successful result Reject only if all fail (AggregateError) Single value Multi-source race, take the fastest success

▶ Example: Promise.all — Passes only if all promises succeed

JAVASCRIPT
async function fetchAll() {
  const urls = [
    'https://api.example.com/a',
    'https://api.example.com/b',
    'https://api.example.com/c',
  ];

  try {
    const results = await Promise.all(
      urls.map(url => fetch(url).then(r => r.json()))
    );
    console.log('All succeeded:', results.length);
  } catch (err) {
    console.error('One failed:', err.message);
  }
}
▶ Try it Yourself

▶ Example: Promise.allSettled — Never Rejects

JAVASCRIPT
async function fetchAllSettled() {
  const tasks = [
    Promise.resolve({ id: 1 }),
    Promise.reject(new Error('Server down')),
    Promise.resolve({ id: 3 }),
  ];

  const results = await Promise.allSettled(tasks);

  const succeeded = results.filter(r => r.status === 'fulfilled');
  const failed = results.filter(r => r.status === 'rejected');

  console.log(`Succeeded: ${succeeded.length}, Failed: ${failed.length}`);
  failed.forEach(r => console.error('Reason:', r.reason.message));
}
▶ Try it Yourself

▶ Example: Promise.race — Timeout Control

JAVASCRIPT
function fetchWithTimeout(url, ms) {
  const fetchTask = fetch(url).then(r => r.json());
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms)
  );
  return Promise.race([fetchTask, timeout]);
}

async function demo() {
  try {
    const data = await fetchWithTimeout('https://api.example.com/slow', 3000);
    console.log(data);
  } catch (err) {
    console.error(err.message);
  }
}
▶ Try it Yourself

▶ Example: Promise.any — Retrieving a Success Result from Multiple Sources in a Race Condition

JAVASCRIPT
async function fastestMirror() {
  const mirrors = [
    fetch('https://mirror1.example.com/data').then(r => r.json()),
    fetch('https://mirror2.example.com/data').then(r => r.json()),
    fetch('https://mirror3.example.com/data').then(r => r.json()),
  ];

  try {
    const result = await Promise.any(mirrors);
    console.log('Fastest response:', result);
  } catch (err) {
    console.error('All mirrors failed:', err.errors.length);
  }
}
▶ Try it Yourself

▶ Example: (2) Mermaid: Comparison of Promise Chaining Method Execution

100%
flowchart TB
    subgraph all["Promise.all"]
        A1["Task A ✅"] --- A2["Task B ✅"] --- A3["Task C ✅"]
        AR["→ [A, B, C] ✅"]
    end

    subgraph race["Promise.race"]
        R1["Task A ⏱ 1s"] --- R2["Task B ⏱ 3s"] --- R3["Task C ⏱ 2s"]
        RR["→ A ✅ (Fastest)"]
    end

    subgraph settled["Promise.allSettled"]
        S1["Task A ✅"] --- S2["Task B ❌"] --- S3["Task C ✅"]
        SR["→ [{fulfilled:A}, {rejected:B}, {fulfilled:C}] ✅"]
    end

    subgraph any["Promise.any"]
        N1["Task A ❌"] --- N2["Task B ✅ ⏱ 2s"] --- N3["Task C ✅ ⏱ 3s"]
        NR["→ B ✅ (Success as Quickly as Possible)"]
    end

    all --> AR
    race --> RR
    settled --> SR
    any --> NR

    style AR fill:#c8e6c9
    style RR fill:#c8e6c9
    style SR fill:#fff9c4
    style NR fill:#c8e6c9


5. Error Handling with async/await

async/await makes asynchronous code look like synchronous code, and error handling follows suit try/catch—but there are a few pitfalls.

(1) Comparison of Error Handling Models

Pattern Implementation Advantages Disadvantages
Callback if (err) { handle } Simple and intuitive Deep nesting, easy to overlook
Promise.catch() promise.then().catch() Chained, reusable Still complex when nested
async/await + try/catch try { await } catch {} Synchronous style, good readability Each await must be wrapped
Wrapper Function const [err, data] = await to(promise) No try/catch, concise Requires importing helper functions

▶ Example: Wrapping an async function with try/catch

JAVASCRIPT
const fs = require('fs').promises;

async function safeReadFile(path) {
  try {
    const data = await fs.readFile(path, 'utf8');
    return { ok: true, data };
  } catch (err) {
    return { ok: false, error: err.message };
  }
}

async function main() {
  const result = await safeReadFile('missing.txt');
  if (!result.ok) {
    console.error('Failed:', result.error);
    return;
  }
  console.log('Content:', result.data);
}

main();
▶ Try it Yourself

▶ Example: Error-wrapping function without try/catch

JAVASCRIPT
function to(promise) {
  return promise
    .then(data => [null, data])
    .catch(err => [err, null]);
}

async function main() {
  const fs = require('fs').promises;

  const [err, data] = await to(fs.readFile('config.json', 'utf8'));
  if (err) {
    console.error('Read failed:', err.message);
    return;
  }
  console.log('Config:', data);
}

main();
▶ Try it Yourself

6. Concurrency Control

(1) Sequential vs. Parallel vs. Constrained Concurrency

Execution Method Total Time (N tasks, each taking T time) Advantages Disadvantages Suitable Scenarios
Sequential execution N × T Simple, resource-efficient Slow Tasks with dependencies
Fully parallel ≈ T Fastest High peak resource usage; may be throttled Few independent tasks
Concurrency-limited ≈ N/concurrency × T Balances speed and resources Slightly more complex to implement Large value of independent tasks, API rate limiting

▶ Example: Sequential Execution

JAVASCRIPT
const fs = require('fs').promises;

async function sequential() {
  const files = ['a.txt', 'b.txt', 'c.txt'];
  const results = [];

  for (const file of files) {
    const data = await fs.readFile(file, 'utf8');
    results.push(data);
  }

  console.log('Results:', results.length);
}
▶ Try it Yourself

▶ Example: Constrained Concurrency Control Function

JAVASCRIPT
async function limitConcurrency(tasks, limit) {
  const results = [];
  const executing = new Set();

  for (const task of tasks) {
    const p = task().then(result => {
      executing.delete(p);
      return result;
    });
    executing.add(p);
    results.push(p);

    if (executing.size >= limit) {
      await Promise.race(executing);
    }
  }

  return Promise.all(results);
}
▶ Try it Yourself

▶ Example: Using the Limited Concurrent Downloads API

JAVASCRIPT
async function fetchApi(url) {
  const res = await fetch(url);
  return res.json();
}

async function batchFetch() {
  const urls = Array.from({ length: 100 }, (_, i) =>
    `https://api.example.com/item/${i + 1}`
  );

  const tasks = urls.map(url => () => fetchApi(url));
  const results = await limitConcurrency(tasks, 5);

  console.log(`Fetched ${results.length} items`);
}
▶ Try it Yourself

7. Best Practices for Chaining Promises

(1) Rules for Chain Calls

Rule Description Counterexample
Always return a Promise Ensure chaining promise.then(() => { doSomething() }) No return
Always handle errors Add .catch() at the end A chained block without .catch() results in uncaught errors
Avoid Nesting Flatten .then() Chains .then(() => { return p.then(...) })
Replace long chains with async/await Replace more than 3 .then() calls with await More than 5 levels of nested .then() calls
Note: A throw in .then() will be caught by the next .catch() because I thought a throw wouldn't interrupt the flow

▶ Example: Flat Chain Call

JAVASCRIPT
const fs = require('fs').promises;

function processFile(path) {
  return fs.readFile(path, 'utf8')
    .then(data => data.trim())
    .then(data => data.toUpperCase())
    .then(data => fs.writeFile('output.txt', data))
    .then(() => console.log('Saved'))
    .catch(err => console.error('Error:', err.message));
}

processFile('input.txt');
▶ Try it Yourself

8. Comprehensive Example: A Concurrency-Limited Batch File Processing Tool

Build a tool: Read a directory → Limit to 3 concurrent file processes → Collect results → Output statistics.

JAVASCRIPT
const fs = require('fs').promises;
const path = require('path');

async function processFile(filePath) {
  const stat = await fs.stat(filePath);
  const content = await fs.readFile(filePath, 'utf8');
  const lines = content.split('\n').length;
  const words = content.split(/\s+/).filter(Boolean).length;
  return {
    file: path.basename(filePath),
    size: stat.size,
    lines,
    words,
  };
}

async function limitConcurrency(tasks, limit) {
  const results = [];
  const executing = new Set();

  for (const task of tasks) {
    const p = task().then(result => {
      executing.delete(p);
      return result;
    });
    executing.add(p);
    results.push(p);

    if (executing.size >= limit) {
      await Promise.race(executing);
    }
  }

  return Promise.all(results);
}

async function batchProcessDir(dirPath, concurrency = 3) {
  console.log(`Scanning directory: ${dirPath}`);

  const files = await fs.readdir(dirPath);
  const filePaths = files
    .filter(f => f.endsWith('.txt') || f.endsWith('.md') || f.endsWith('.json'))
    .map(f => path.join(dirPath, f));

  if (filePaths.length === 0) {
    console.log('No matching files found.');
    return;
  }

  const tasks = filePaths.map(fp => () => processFile(fp));
  const results = await limitConcurrency(tasks, concurrency);

  console.log('\n--- File Statistics ---');
  console.log('File'.padEnd(20) + 'Size'.padEnd(10) + 'Lines'.padEnd(8) + 'Words');
  console.log('-'.repeat(46));

  let totalLines = 0;
  let totalWords = 0;
  let totalSize = 0;

  for (const r of results) {
    console.log(
      r.file.padEnd(20) +
      String(r.size).padEnd(10) +
      String(r.lines).padEnd(8) +
      String(r.words)
    );
    totalLines += r.lines;
    totalWords += r.words;
    totalSize += r.size;
  }

  console.log('-'.repeat(46));
  console.log(
    'TOTAL'.padEnd(20) +
    String(totalSize).padEnd(10) +
    String(totalLines).padEnd(8) +
    String(totalWords)
  );
  console.log(`\nProcessed ${results.length} files (concurrency: ${concurrency})`);
}

batchProcessDir('./data', 3).catch(err => console.error('Fatal:', err.message));

Performance:

TEXT 📖 Display only
Scanning directory: ./data

--- File Statistics ---
File                Size      Lines   Words
----------------------------------------------
config.json         256       12      42
readme.md           1024      45      312
notes.txt           512       28      178
----------------------------------------------
TOTAL               1792      85      532

Processed 3 files (concurrency: 3)

❓ FAQ

Q What happens if one promise fails in Promise.all?
A The entire operation fails immediately (fast-fail), and only the first error is returned. Use Promise.allSettled when you need all results.
Q How can I limit the value of concurrent async/await operations?
A Implement a limitConcurrency function that uses a Set to track active Promises and Promise.race to control the value of concurrent operations; in production environments, you can also use the p-limit library.
Q Can await only be used inside async functions?
A Yes. ES2022 introduced top-level await, which allows await to be used directly at the top level of an ES Module, but in CommonJS modules, it still needs to be wrapped inside an async function.
Q Can util.promisify convert all callback functions?
A No. It only works with error-first callbacks, i.e., those in the (err, result) => {} format. Custom multi-argument callbacks must be wrapped manually.
Q What are some practical uses for Promise.race?
A The most common use is for timeout control—pitting a business Promise against a timer Promise in a race; it can also be used to select the fastest response among multiple requests.
Q Does Promise.allSettled return results in the same order as the input?
A Yes, the order of the result array strictly matches the order of the input array of Promises, regardless of when each Promise resolves.
Q What does an async function return?
A An async function always returns a Promise. Even if it returns a regular value, it is automatically wrapped in Promise.resolve(value).

📖 Summary


📝 Exercises

  1. Complete all the code examples in this lesson and make sure each one runs correctly.
  2. Modify the comprehensive example and add your own extensions
  3. Review the official documentation, identify 1–2 APIs not covered in this lesson, and write test code for them.
  4. Reflection: How would you apply what you’ve learned in this lesson to a real-world project?
  5. Try to combine what you’ve learned in this lesson with material from previous lessons to build a small project.
Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏