Node.js: Promise 与 async/await

最后更新:2026-08-26

1. 你将学到



2. 故事:Charlie 的批量数据下载

Charlie 负责从 100 个外部 API 下载配置数据。起初他用 for 循环逐个请求,每个请求约 30 秒,100 个全部跑完需要 50 分钟。后来他改用 Promise.all 并行请求,总耗时降到 30 秒——但服务器立刻返回 429 Too Many Requests。Charlie 意识到需要限制并发数,最终将并发控制在 5 个,总耗时约 10 分钟,既高效又不触发限流。

这个故事揭示了异步编程的核心权衡:速度 vs 资源。Promise 和 async/await 就是掌控这个平衡的工具。



3. fs.promises API

Node.js 从 v10 起提供 fs.promises,将文件系统操作封装为返回 Promise 的方法,告别回调地狱。

(1) (3.1) fs 回调 vs fs.promises vs promisify

特性 fs 回调 fs.promises util.promisify(fs.xxx)
返回值 void,结果通过回调传入 Promise Promise
错误处理 回调第一个参数 err try/catch 或 .catch() try/catch 或 .catch()
代码风格 嵌套回调 扁平 async/await 扁平 async/await
可用版本 全部版本 v10+ v8+
典型用法 fs.readFile(path, (err, data) => {}) await fs.promises.readFile(path) const readFile = promisify(fs.readFile); await readFile(path)

▶ 示例: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();
▶ 试一试

▶ 示例: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');
▶ 试一试

▶ 示例:util.promisify 转换回调函数

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();
▶ 试一试

4. Promise 组合方法

四个静态方法决定了多个 Promise 之间的协作方式,选择错误的组合方法会导致截然不同的结果。

(1) (4.1) 四种组合方法对比

方法 全部成功时 有失败时 返回值 典型用途
Promise.all 返回全部结果数组 第一个失败即拒绝,整体 reject 结果数组 多个任务必须全部完成
Promise.race 返回最先完成的结果 最先失败即拒绝 单个值 超时控制、最快响应
Promise.allSettled 返回全部结果 不拒绝,包含失败信息 {status, value/reason}[] 需要全部结果,不论成败
Promise.any 返回最先成功的结果 全部失败才拒绝(AggregateError) 单个值 多源竞速,取最快成功

▶ 示例:Promise.all — 全部成功才通过

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);
  }
}
▶ 试一试

▶ 示例:Promise.allSettled — 永不拒绝

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));
}
▶ 试一试

▶ 示例:Promise.race — 超时控制

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);
  }
}
▶ 试一试

▶ 示例:Promise.any — 多源竞速取成功

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);
  }
}
▶ 试一试

▶ 示例:(2) (4.2) Mermaid:Promise 组合方法执行对比

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 ✅ (最快)"]
    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 ✅ (最快成功)"]
    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. async/await 错误处理

async/await 让异步代码看起来像同步代码,错误处理也随之变成 try/catch——但有几个易错点。

(1) (5.1) 错误处理模式对比

模式 写法 优点 缺点
回调 if (err) { handle } 简单直观 嵌套深、易遗漏
Promise .catch() promise.then().catch() 链式、可复用 嵌套时仍复杂
async/await + try/catch try { await } catch {} 同步风格、可读性好 每个 await 都需包裹
包装函数 const [err, data] = await to(promise) 无 try/catch、简洁 需引入工具函数

▶ 示例:try/catch 包裹 async 函数

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/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();
▶ 试一试

6. 并发控制

(1) (6.1) 顺序 vs 并行 vs 受限并发

执行方式 总耗时(N个任务,单个耗时T) 优点 缺点 适用场景
顺序执行 N × T 简单、不耗资源 有依赖关系的任务
完全并行 ≈ T 最快 资源峰值高、可能被限流 少量独立任务
受限并发 ≈ N/concurrency × T 平衡速度与资源 实现稍复杂 大量独立任务、API限流

▶ 示例:顺序执行

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);
}
▶ 试一试

▶ 示例:受限并发控制函数

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);
}
▶ 试一试

▶ 示例:使用受限并发下载 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`);
}
▶ 试一试

7. Promise 链式调用最佳实践

(1) (7.1) 链式调用规则

规则 说明 反例
始终返回 Promise 确保链式传递 promise.then(() => { doSomething() }) 未 return
始终处理错误 末尾加 .catch() 链式无 .catch() 导致未捕获
避免嵌套 扁平化 .then() 链 .then(() => { return p.then(...) })
用 async/await 替代长链 3 个以上 .then() 改用 await 超 5 层 .then() 嵌套
注意 .then() 中的 throw 会被下一个 .catch() 捕获 以为 throw 不会中断

▶ 示例:扁平链式调用

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');
▶ 试一试

8. 综合示例:并发受限的批量文件处理工具

构建一个工具:读取目录 → 限制 3 个并发处理文件 → 收集结果 → 输出统计。

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));

运行效果:

TEXT 📖 仅展示
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)

❓ 常见问题

Q Promise.all 中一个失败会怎样?
A 整体立即拒绝(fast-fail),只返回第一个错误。需要全部结果时使用 Promise.allSettled。
Q 如何限制 async/await 的并发数?
A 实现一个 limitConcurrency 函数,用 Set 跟踪运行中的 Promise,配合 Promise.race 控制同时运行的数量;生产环境也可用 p-limit 库。
Q await 只能在 async 函数中使用吗?
A 是的。ES2022 引入了顶层 await(top-level await),允许在 ES Module 顶层直接使用 await,但 CommonJS 模块中仍需包裹在 async 函数内。
Q util.promisify 能转换所有回调函数吗?
A 不能。它只适用于错误优先回调(error-first callback)格式,即 (err, result) => {} 形式。自定义多参数回调需要手动包装。
Q Promise.race 有什么实际用途?
A 最常见的是超时控制——将业务 Promise 与一个定时器 Promise 竞速;也可用于多源请求取最快响应。
Q Promise.allSettled 返回的结果顺序和输入一致吗?
A 是的,结果数组的顺序与传入的 Promise 数组严格一致,不论各自完成先后。
Q async 函数返回的是什么?
A async 函数始终返回一个 Promise。即使 return 一个普通值,也会被自动包装为 Promise.resolve(value)。

📖 小节


📝 作业

  1. 完成本课所有代码示例,确保每个示例都能正确运行
  2. 修改综合示例,添加自己的扩展功能
  3. 查阅官方文档,找出本课未涉及的1-2个API并编写测试代码
  4. 思考:在实际项目中,你会如何应用本课学到的知识?
  5. 尝试将本课知识与前面课程的内容结合,构建一个小项目
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

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

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