您现在的位置是:网站首页 > 异步错误处理文章详情
异步错误处理
陈川
【
JavaScript
】
16158人已围观
6068字
异步错误处理的挑战
JavaScript的异步特性带来了独特的错误处理难题。回调函数、Promise和async/await各自有不同的错误传播机制。传统的try-catch在异步上下文中无法捕获回调函数内部的错误,这导致许多开发者不得不采用深度嵌套的错误优先回调模式。
// 传统回调的错误处理
fs.readFile('nonexistent.txt', (err, data) => {
if (err) {
console.error('读取文件出错:', err.message);
return;
}
console.log(data);
});
Promise的错误捕获
Promise通过.catch()方法提供了更优雅的错误处理方式。当Promise链中的任何环节抛出错误或返回rejected状态的Promise时,控制权会传递给最近的.catch()处理器。这种机制避免了回调地狱,使错误处理流程更加线性。
fetch('https://api.example.com/data')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => processData(data))
.catch(error => {
console.error('请求失败:', error);
// 可以在这里进行错误恢复或上报
reportErrorToServer(error);
});
async/await的try-catch模式
async/await语法让异步代码看起来像同步代码,错误处理也可以使用传统的try-catch结构。这种模式更符合直觉,特别是在需要处理多个异步操作时,能显著提升代码可读性。
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
const user = await response.json();
const posts = await fetch(`/api/posts?userId=${user.id}`);
return { user, posts };
} catch (error) {
console.error('获取用户数据失败:', error);
// 可以在这里决定是继续抛出错误还是返回默认值
return { user: null, posts: [] };
}
}
未处理的Promise拒绝
Node.js和浏览器环境对未处理的Promise拒绝有不同的处理方式。在Node.js中,未处理的拒绝会导致进程退出,而在浏览器中可能只是静默失败。应该始终为Promise添加错误处理,或者使用全局捕获机制。
// 浏览器环境
window.addEventListener('unhandledrejection', event => {
console.warn(`未处理的Promise拒绝: ${event.reason}`);
event.preventDefault(); // 阻止默认行为(如控制台报错)
});
// Node.js环境
process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的拒绝:', reason);
});
错误边界与传播策略
在复杂的异步操作中,需要决定错误应该在何处被捕获和处理。有时需要在局部处理错误并继续执行,有时则需要让错误冒泡到更高层级。设计良好的错误传播策略对维护大型应用至关重要。
async function processOrder(orderId) {
try {
const order = await getOrder(orderId);
await validateOrder(order);
await processPayment(order);
await updateInventory(order);
await sendConfirmation(order);
} catch (error) {
if (error instanceof PaymentError) {
await notifyPaymentFailure(order, error);
} else if (error instanceof InventoryError) {
await scheduleRetry(order, error);
} else {
// 其他未知错误向上传播
throw error;
}
}
}
错误类型与自定义错误
创建特定的错误类型有助于更精确地处理不同类别的错误。自定义错误可以携带额外的上下文信息,帮助调试和错误恢复。
class NetworkError extends Error {
constructor(url, status, ...params) {
super(...params);
this.name = 'NetworkError';
this.url = url;
this.status = status;
this.timestamp = new Date();
}
}
async function fetchWithRetry(url, retries = 3) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new NetworkError(url, response.status, '请求失败');
}
return response.json();
} catch (error) {
if (retries > 0 && error instanceof NetworkError) {
await delay(1000);
return fetchWithRetry(url, retries - 1);
}
throw error;
}
}
并行操作的错误处理
当使用Promise.all或Promise.race等组合器处理多个并行异步操作时,错误处理行为会有所不同。Promise.all会在任何一个Promise拒绝时立即拒绝,而Promise.any会在所有Promise都拒绝时才拒绝。
async function fetchMultipleResources(urls) {
try {
// 所有请求必须成功
const responses = await Promise.all(urls.map(url => fetch(url)));
return await Promise.all(responses.map(r => r.json()));
} catch (error) {
// 任何一个请求失败都会进入这里
console.error('部分资源加载失败', error);
throw new PartialDataError('未能获取全部数据');
}
}
async function fetchFromFastestMirror(mirrors) {
try {
// 使用Promise.any获取最快的可用镜像
const response = await Promise.any(
mirrors.map(url => fetch(url).then(r => {
if (!r.ok) throw new Error(`镜像不可用: ${url}`);
return r;
})
);
return response.json();
} catch (error) {
// 所有镜像都不可用时进入这里
throw new NoAvailableMirrorError('所有镜像均不可用');
}
}
错误恢复与重试机制
对于暂时性错误(如网络波动),实现自动重试机制可以提升用户体验。指数退避算法是常用的重试策略,避免对服务器造成过大压力。
async function fetchWithExponentialBackoff(url, maxRetries = 5) {
let attempt = 0;
while (true) {
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (error) {
if (attempt >= maxRetries) throw error;
const delayMs = Math.pow(2, attempt) * 1000 + Math.random() * 1000;
console.warn(`请求失败,${delayMs}ms后重试... (${attempt + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, delayMs));
attempt++;
}
}
}
错误日志与监控
在生产环境中,需要将客户端错误记录到服务器以便分析。要注意避免敏感信息泄露,同时确保错误上报本身不会引发新的错误。
async function logError(error, context = {}) {
try {
const errorData = {
message: error.message,
stack: error.stack,
name: error.name,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
...context
};
await fetch('/api/error-log', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(errorData)
});
} catch (reportingError) {
console.error('无法上报错误:', reportingError);
}
}
window.addEventListener('error', event => {
logError(event.error, { type: 'unhandled' });
});
window.addEventListener('unhandledrejection', event => {
logError(event.reason, { type: 'promise-rejection' });
});
测试异步错误
编写测试用例验证错误处理逻辑的正确性同样重要。现代测试框架提供了专门的方法来测试异步代码中的错误抛出。
// 使用Jest测试异步错误
describe('fetchUserData', () => {
it('应该在API返回404时抛出NotFoundError', async () => {
fetch.mockRejectedValueOnce(new Error('404'));
await expect(fetchUserData(123)).rejects.toThrow(NotFoundError);
});
it('应该在网络错误时自动重试3次', async () => {
fetch.mockRejectedValueOnce(new Error('network'));
fetch.mockRejectedValueOnce(new Error('network'));
fetch.mockResolvedValueOnce({ ok: true, json: () => ({ id: 123 }) });
const result = await fetchUserData(123);
expect(result.id).toBe(123);
expect(fetch).toHaveBeenCalledTimes(3);
});
});