Appearance
常见陷阱:那些年我们踩过的异步坑
异步编程的陷阱往往不会立即暴露,而是在生产环境的高并发下突然爆发。
本章收集了 Node.js 开发中最常见的异步编程错误,帮你提前规避。
陷阱一:循环中的 await
问题
javascript
// 效率极低的写法
async function processItems(items) {
const results = [];
for (const item of items) {
const result = await processItem(item); // 串行!
results.push(result);
}
return results;
}
// 10 个任务,每个 1 秒,总共 10 秒解决方案
并行处理:
javascript
async function processItems(items) {
return Promise.all(items.map(item => processItem(item)));
}
// 10 个任务并行,总共约 1 秒需要顺序时使用 for...of:
javascript
// 只有真正需要顺序时才用串行
async function processOrdered(items) {
for (const item of items) {
await processItem(item); // 必须按顺序
}
}并发控制:
javascript
const pLimit = require('p-limit');
const limit = pLimit(5); // 最多 5 个并发
const results = await Promise.all(
items.map(item => limit(() => processItem(item)))
);陷阱二:忘记 await
问题
javascript
async function saveData(data) {
await validate(data);
db.save(data); // 忘记 await!
console.log('保存成功'); // 其实还没保存完
}更隐蔽的情况:
javascript
async function cleanup() {
files.forEach(async file => {
await fs.unlink(file); // 不会等待
});
console.log('清理完成'); // 其实没清理完
}解决方案
javascript
// 使用 for...of
async function cleanup() {
for (const file of files) {
await fs.unlink(file);
}
console.log('清理完成');
}
// 或 Promise.all
async function cleanup() {
await Promise.all(files.map(file => fs.unlink(file)));
console.log('清理完成');
}陷阱三:在非异步函数中使用 await
问题
javascript
function getData() {
const data = await fetchData(); // SyntaxError!
return data;
}解决方案
javascript
// 改为 async 函数
async function getData() {
const data = await fetchData();
return data;
}
// 或者返回 Promise
function getData() {
return fetchData().then(data => data);
}陷阱四:Promise 构造函数中的 async
问题
javascript
// 这是错误的模式
const promise = new Promise(async (resolve, reject) => {
try {
const data = await fetchData();
resolve(data);
} catch (err) {
reject(err); // 可能不会执行
}
});问题在于:如果 fetchData() 抛出同步错误,它不会触发 reject,而是导致未处理的 Promise 拒绝。
解决方案
javascript
// 直接使用 async 函数
async function getData() {
const data = await fetchData();
return data;
}
// 如果确实需要包装
function getData() {
return fetchData(); // 已经是 Promise
}陷阱五:未处理的 Promise 拒绝
问题
javascript
async function mayFail() {
throw new Error('失败');
}
mayFail(); // 未处理!Node.js 可能崩溃javascript
// 更隐蔽的情况
const promises = items.map(item => processItem(item));
// 忘记 await Promise.all(promises)解决方案
javascript
// 始终处理错误
mayFail().catch(err => console.error(err));
// 或使用 try-catch
try {
await mayFail();
} catch (err) {
console.error(err);
}
// 设置全局处理器作为安全网
process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的 Promise 拒绝:', reason);
});陷阱六:竞态条件
问题
javascript
let cachedUser = null;
async function getUser(id) {
if (!cachedUser) {
cachedUser = await fetchUser(id); // 竞态!
}
return cachedUser;
}
// 多次调用可能触发多次请求
getUser(1);
getUser(1);
getUser(1);解决方案
javascript
let userPromise = null;
async function getUser(id) {
if (!userPromise) {
userPromise = fetchUser(id); // 缓存 Promise
}
return userPromise;
}
// 或使用 Map 管理多个请求
const pendingRequests = new Map();
async function getUser(id) {
if (!pendingRequests.has(id)) {
pendingRequests.set(id, fetchUser(id).finally(() => {
pendingRequests.delete(id);
}));
}
return pendingRequests.get(id);
}陷阱七:this 丢失
问题
javascript
class UserService {
constructor() {
this.baseUrl = '/api';
}
async getUser(id) {
// this.baseUrl 在某些情况下是 undefined
return fetch(`${this.baseUrl}/users/${id}`);
}
}
const service = new UserService();
const handler = service.getUser; // this 丢失
await handler(1); // TypeError: Cannot read property 'baseUrl' of undefined解决方案
javascript
// 方案1:箭头函数
class UserService {
getUser = async (id) => {
return fetch(`${this.baseUrl}/users/${id}`);
}
}
// 方案2:绑定
const handler = service.getUser.bind(service);
// 方案3:包装
const handler = (id) => service.getUser(id);陷阱八:错误被吞掉
问题
javascript
async function process() {
try {
await step1();
await step2();
await step3();
} catch (err) {
console.log('出错了'); // 错误信息丢失
}
}解决方案
javascript
async function process() {
try {
await step1();
await step2();
await step3();
} catch (err) {
console.error('处理失败:', err.message);
console.error(err.stack);
throw err; // 或重新抛出
}
}陷阱九:内存泄漏
问题
javascript
// 事件监听器未移除
async function watchFile(path) {
const watcher = fs.watch(path);
watcher.on('change', async () => {
await processChange();
});
// watcher 永远不会被清理
}javascript
// 未完成的 Promise 持有引用
const pending = new Map();
async function request(id) {
pending.set(id, fetch(`/api/${id}`));
// 永远不会删除
}解决方案
javascript
async function watchFile(path, signal) {
const watcher = fs.watch(path);
const handler = async () => {
await processChange();
};
watcher.on('change', handler);
// 提供清理方法
signal.addEventListener('abort', () => {
watcher.close();
});
}
// 使用
const controller = new AbortController();
watchFile('/path', controller.signal);
// 清理时
controller.abort();陷阱十:超时处理不当
问题
javascript
// 超时后请求仍在继续
async function fetchWithTimeout(url, timeout) {
return Promise.race([
fetch(url),
new Promise((_, reject) => {
setTimeout(() => reject(new Error('超时')), timeout);
})
]);
}
// 问题:超时后 fetch 仍在执行解决方案
javascript
async function fetchWithTimeout(url, timeout) {
const controller = new AbortController();
const timeoutId = setTimeout(() => {
controller.abort();
}, timeout);
try {
const response = await fetch(url, { signal: controller.signal });
clearTimeout(timeoutId);
return response;
} catch (err) {
clearTimeout(timeoutId);
if (err.name === 'AbortError') {
throw new Error('请求超时');
}
throw err;
}
}陷阱十一:并发修改共享状态
问题
javascript
let balance = 100;
async function withdraw(amount) {
if (balance >= amount) {
await delay(100); // 模拟异步操作
balance -= amount;
return true;
}
return false;
}
// 同时取款
await Promise.all([
withdraw(80), // 检查时 balance = 100,通过
withdraw(80) // 检查时 balance = 100,通过
]);
// 结果:balance = -60,超额取款!解决方案
javascript
const { Mutex } = require('async-mutex');
const mutex = new Mutex();
async function withdraw(amount) {
const release = await mutex.acquire();
try {
if (balance >= amount) {
await delay(100);
balance -= amount;
return true;
}
return false;
} finally {
release();
}
}最佳实践清单
- 始终处理 Promise 错误:使用
.catch()或try-catch - 并行优先:除非有顺序要求,否则使用
Promise.all - 控制并发:使用限流避免资源耗尽
- 设置超时:网络请求必须有超时
- 使用 AbortController:支持取消操作
- 避免共享可变状态:使用锁或消息队列
- 记录完整错误信息:不要吞掉错误
- 测试异步代码:包括错误路径
- 设置全局错误处理器:作为最后防线
- 定期检查内存:发现泄漏及时修复
本章小结
- 循环中的
await导致串行执行,应使用Promise.all并行化 - 忘记
await是常见错误,特别是在forEach中 - 未处理的 Promise 拒绝可能导致应用崩溃
- 竞态条件通过缓存 Promise 而非结果来避免
- 使用
AbortController实现可取消的操作 - 并发修改共享状态需要加锁
掌握这些陷阱和解决方案,你的异步代码将更加健壮可靠。
下一部分,我们将深入学习 Node.js 的核心模块。