为什么异步错误处理很棘手
同步错误会被自然地捕获并沿调用栈传播。而异步错误——即 Promise 或 async 函数内部的错误——行为不同,处理上的空白可能导致静默失败、未处理的拒绝以及难以调试的崩溃。
本指南涵盖了现代异步 JavaScript 和 TypeScript 中处理错误的每一种模式。

基础:使用 Async/Await 的 Try-Catch
async function fetchUser(id) {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
}
return await res.json();
} catch (err) {
console.error('获取用户失败:', err);
throw err; // 重新抛出,以便调用者处理
}
}
await 关键字将拒绝的 Promise "解包"为抛出的错误。如果没有 await,你操作的是 Promise 对象,错误不会被 try-catch 捕获。
常见错误:
// ❌ 错误未被捕获——缺少 await
async function buggy() {
try {
fetch('/api/data') // 返回了 Promise 但未 await
.then(res => res.json()); // 这里的拒绝不会被捕获
} catch (err) {
// 对于 fetch 错误,这里永远不会执行
}
}
// ✅ 修复后
async function correct() {
try {
const res = await fetch('/api/data');
return await res.json();
} catch (err) {
// 捕获网络错误和响应解析错误
}
}
需要处理的错误类型
async function robustFetch(url) {
try {
const res = await fetch(url);
// HTTP 错误(4xx, 5xx)默认不会抛出——你必须检查
if (!res.ok) {
const body = await res.text();
throw new HttpError(res.status, res.statusText, body);
}
return await res.json();
} catch (err) {
if (err instanceof HttpError) {
// 处理服务器错误(404, 500 等)
throw err;
}
if (err instanceof TypeError && err.message.includes('fetch')) {
// 网络错误(离线、DNS 失败、CORS)
throw new NetworkError('网络不可用', { cause: err });
}
if (err instanceof SyntaxError) {
// JSON 解析错误
throw new ParseError('无效的 JSON 响应', { cause: err });
}
throw err; // 未知错误——重新抛出
}
}
class HttpError extends Error {
constructor(status, statusText, body) {
super(`HTTP ${status}: ${statusText}`);
this.name = 'HttpError';
this.status = status;
this.body = body;
}
}
Promise.all 与 Promise.allSettled
Promise.all 在任何 Promise 拒绝时立即拒绝——其他 Promise 被放弃:
// ❌ 如果任何一个请求失败,所有结果都丢失
const [users, posts, comments] = await Promise.all([
fetchUsers(),
fetchPosts(),
fetchComments(),
]);
Promise.allSettled 等待所有 Promise 完成,并分别给出每个结果:
// ✅ 获取成功的结果,处理失败的结果
const results = await Promise.allSettled([
fetchUsers(),
fetchPosts(),
fetchComments(),
]);
const users = results[0].status === 'fulfilled' ? results[0].value : [];
const posts = results[1].status === 'fulfilled' ? results[1].value : [];
const errors = results
.filter(r => r.status === 'rejected')
.map(r => r.reason);
if (errors.length > 0) {
console.warn('部分请求失败:', errors);
}
当所有请求都必须成功时使用 Promise.all。当部分成功可接受时使用 Promise.allSettled。

超时模式
fetch 没有内置超时。使用 AbortController:
async function fetchWithTimeout(url, timeoutMs = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const res = await fetch(url, { signal: controller.signal });
clearTimeout(timeoutId);
return await res.json();
} catch (err) {
if (err.name === 'AbortError') {
throw new Error(`请求在 ${timeoutMs}ms 后超时`);
}
throw err;
} finally {
clearTimeout(timeoutId); // 始终清理
}
}
或者使用 Promise.race:
function timeout(ms) {
return new Promise((_, reject) =>
setTimeout(() => reject(new Error(`在 ${ms}ms 后超时`)), ms)
);
}
async function fetchWithRaceTimeout(url, ms = 5000) {
return Promise.race([
fetch(url).then(r => r.json()),
timeout(ms),
]);
}
带指数退避的重试逻辑
async function fetchWithRetry(url, options = {}) {
const { maxRetries = 3, baseDelay = 1000, retryOn = [429, 503] } = options;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const res = await fetch(url, options);
if (retryOn.includes(res.status) && attempt < maxRetries) {
const delay = baseDelay * 2 ** attempt + Math.random() * 1000;
console.log(`在 ${Math.round(delay)}ms 后重试(第 ${attempt + 1} 次)`);
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
if (attempt === maxRetries) throw err;
const isRetryable = err.name === 'TypeError'; // 网络错误
if (!isRetryable) throw err;
const delay = baseDelay * 2 ** attempt;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
避免未处理的 Promise 拒绝
未处理的 Promise 拒绝会导致 Node.js 崩溃(自 v15 起),并在浏览器中记录警告。
// ❌ 未处理的拒绝——没有 .catch() 也没有带 try-catch 的 await
const promise = fetchData();
// promise 可能拒绝,但没有任何处理
// ✅ 处理它
fetchData().catch(err => console.error('fetchData 失败:', err));
// ✅ 或者使用 async/await
async function init() {
try {
await fetchData();
} catch (err) {
console.error('fetchData 失败:', err);
}
}
// ✅ 全局处理程序作为最后手段
process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的拒绝:', reason);
// 不要吞掉——记录并可选地退出
process.exit(1);
});

TypeScript:类型化错误处理
TypeScript 不会为 catch 块中的错误提供类型——它们始终是 unknown:
async function fetchData(url: string): Promise<Data> {
try {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json() as Data;
} catch (err) {
// 在 TypeScript 严格模式下,err 是 'unknown'
if (err instanceof Error) {
throw new Error(`获取失败: ${err.message}`);
}
throw new Error('未知错误');
}
}
Result 模式(无异常):
type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };
async function safeFetch<T>(url: string): Promise<Result<T>> {
try {
const res = await fetch(url);
if (!res.ok) {
return { ok: false, error: new Error(`HTTP ${res.status}`) };
}
const data = await res.json() as T;
return { ok: true, value: data };
} catch (err) {
return { ok: false, error: err instanceof Error ? err : new Error(String(err)) };
}
}
// 使用——调用处无需 try-catch
const result = await safeFetch<User>('/api/user/1');
if (!result.ok) {
console.error('失败:', result.error.message);
return;
}
const user = result.value; // 类型为 User
React 中的错误边界
对于 React 组件中的异步错误,使用错误边界:
// React Query 优雅地处理异步错误
const { data, error, isLoading } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
retry: 3,
retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30000),
});
if (error) {
return <ErrorMessage message={error.message} />;
}
快速参考
| 场景 | 解决方案 |
|---|---|
| 单个异步操作 | try/catch 配合 await |
| 多个操作,全部必须成功 | Promise.all + try/catch |
| 多个操作,部分失败可接受 | Promise.allSettled |
| 需要超时 | AbortController + 超时 |
| 临时故障(速率限制、503) | 带指数退避的重试 |
| 防止未处理的拒绝 | 始终 .catch() 或在 try/catch 中 await |
→ 使用 JSON Viewer 检查 API 错误响应并理解其结构。