Node.js: Promise 与 async/await
最后更新:2026-08-26
1. 你将学到
- fs.promises API 的核心方法与使用场景
- Promise.all / race / allSettled / any 的行为差异与选择
- async/await 配合 try/catch 的错误处理模式
- 并发控制:限制同时运行的异步任务数量
- 顺序执行、并行执行、受限并发的适用场景
- Promise 链式调用的最佳实践
- util.promisify 将回调风格转为 Promise
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 组合方法执行对比
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)。
📖 小节
- 你将学到的核心概念与使用方法
- 故事:Charlie 的批量数据下载的核心概念与使用方法
- fs.promises API的核心概念与使用方法
- Promise 组合方法的核心概念与使用方法
- async/await 错误处理的核心概念与使用方法
- 并发控制的核心概念与使用方法
- Promise 链式调用最佳实践的核心概念与使用方法
- 综合示例:并发受限的批量文件处理工具的核心概念与使用方法
📝 作业
- 完成本课所有代码示例,确保每个示例都能正确运行
- 修改综合示例,添加自己的扩展功能
- 查阅官方文档,找出本课未涉及的1-2个API并编写测试代码
- 思考:在实际项目中,你会如何应用本课学到的知识?
- 尝试将本课知识与前面课程的内容结合,构建一个小项目