Promise 与 async/await:使用方式、错误传播与捕获
JavaScript 中很多操作不会立即完成,例如网络请求、定时器和文件读取。Promise 用来表示一个“现在还没有结果,但未来会产生结果”的异步操作;async/await 则是在 Promise 之上提供的一种更接近同步代码的写法。
async/await 并不是另一套异步机制,而是建立在 Promise 之上的语法糖。
一、Promise 的定义与状态
一个 Promise 只能处于三种状态之一:
pending:等待中
fulfilled:成功完成
rejected:执行失败
状态只能改变一次:
1 2
| pending → fulfilled pending → rejected
|
一旦进入 fulfilled 或 rejected,状态和最终结果便不会再改变。
创建 Promise
1 2 3 4 5 6 7 8 9
| const promise = new Promise((resolve, reject) => { const success = true;
if (success) { resolve("操作成功"); } else { reject(new Error("操作失败")); } });
|
resolve(value):把 Promise 变成 fulfilled,并提供成功值。
reject(error):把 Promise 变成 rejected,并提供失败原因。
推荐使用 Error 对象,而不是拒绝一个普通字符串:
1 2
| reject(new Error("用户不存在")); reject("用户不存在");
|
Error 对象包含错误类型、错误信息和调用栈,更便于定位问题。
二、then、catch 和 finally
1. 使用 then 处理成功结果
1 2 3
| Promise.resolve(10).then(value => { console.log(value); });
|
.then() 会返回一个新的 Promise,所以可以链式调用:
1 2 3 4 5 6
| Promise.resolve(10) .then(value => value * 2) .then(value => value + 5) .then(value => { console.log(value); });
|
前一个 .then() 的返回值会成为下一个 .then() 接收到的值:
2. then 返回 Promise 时会自动等待
1 2 3 4 5 6 7 8 9 10 11
| function delayDouble(value) { return new Promise(resolve => { setTimeout(() => resolve(value * 2), 1000); }); }
Promise.resolve(10) .then(value => delayDouble(value)) .then(result => { console.log(result); });
|
下一个 .then() 不会收到原始 Promise 对象,而会等待它完成并收到成功值。这种自动展开 Promise 的行为,是 Promise 链能够表达连续异步操作的关键。
三、Promise 错误如何产生和传播
Promise 链中,以下情况都会使当前步骤返回的 Promise 变成 rejected:
- 返回一个 rejected Promise。
- 在回调中直接抛出异常。
1 2 3 4 5
| Promise.resolve() .then(() => Promise.reject(new Error("数据库连接失败"))) .catch(error => { console.error(error.message); });
|
直接抛出异常也会得到相同的传播效果:
1 2 3 4 5 6 7
| Promise.resolve() .then(() => { throw new Error("数据格式错误"); }) .catch(error => { console.error(error.message); });
|
在 Promise 回调中,下面两种写法的传播效果近似:
1 2
| throw error; return Promise.reject(error);
|
错误会沿 Promise 链向后传播
如果某一步失败,之后没有错误处理器的成功回调会被跳过,直到找到最近的 .catch():
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| Promise.resolve("开始") .then(value => { console.log(value); throw new Error("第二步失败"); }) .then(() => { console.log("不会执行"); }) .then(() => { console.log("也不会执行"); }) .catch(error => { console.error("捕获到:", error.message); });
|
输出:
.catch(handler) 基本等价于 .then(undefined, handler)。
catch 可以恢复,也可以继续传播错误
如果 .catch() 返回普通值,它生成的新 Promise 会变为 fulfilled,后续 .then() 可以继续执行:
1 2 3 4 5 6 7 8
| Promise.reject(new Error("请求失败")) .catch(error => { console.error(error.message); return "默认数据"; }) .then(value => { console.log(value); });
|
如果希望上层仍然知道发生了错误,应重新抛出:
1 2 3 4 5 6 7 8
| Promise.reject(new Error("请求失败")) .catch(error => { console.error("记录日志:", error.message); throw error; }) .catch(error => { console.error("上层捕获:", error.message); });
|
finally 的行为
.finally() 无论 Promise 成功还是失败都会运行,适合关闭加载状态或释放资源:
1 2 3 4 5 6
| requestData() .then(data => console.log(data)) .catch(error => console.error(error)) .finally(() => { console.log("请求结束"); });
|
它通常不改变原来的结果:
1 2 3
| Promise.resolve(10) .finally(() => console.log("清理资源")) .then(value => console.log(value));
|
但如果 .finally() 自己抛出错误,新错误会覆盖原结果:
1 2 3 4 5 6 7
| Promise.resolve("原始结果") .finally(() => { throw new Error("清理失败"); }) .catch(error => { console.log(error.message); });
|
四、Promise 构造函数的捕获范围
Promise 执行器中的同步异常会自动转化为 rejection:
1 2 3 4 5 6 7
| const promise = new Promise(() => { throw new Error("同步异常"); });
promise.catch(error => { console.log(error.message); });
|
但是,稍后执行的异步回调所抛出的异常不会自动被外层 Promise 捕获:
1 2 3 4 5 6 7 8 9
| const promise = new Promise(() => { setTimeout(() => { throw new Error("异步异常"); }, 1000); });
promise.catch(() => { });
|
因为定时器回调运行时,Promise 执行器早已结束。需要在异步回调中主动调用 reject:
1 2 3 4 5 6 7 8 9
| const promise = new Promise((resolve, reject) => { setTimeout(() => { try { throw new Error("异步异常"); } catch (error) { reject(error); } }, 1000); });
|
如果异步 API 本身已经返回 Promise,不要再无意义地包装一层:
1 2 3 4 5 6 7 8 9 10 11
| function getData() { return new Promise((resolve, reject) => { fetch("/api/data").then(resolve).catch(reject); }); }
function getData() { return fetch("/api/data"); }
|
五、async 函数
在函数前添加 async,该函数就一定会返回 Promise:
1 2 3 4 5 6 7
| async function getNumber() { return 10; }
const result = getNumber(); console.log(result instanceof Promise); result.then(console.log);
|
它大体等价于:
1 2 3
| function getNumber() { return Promise.resolve(10); }
|
如果 async 函数抛出错误,它返回的 Promise 会变成 rejected:
1 2 3 4 5 6 7
| async function getUser() { throw new Error("用户不存在"); }
getUser().catch(error => { console.log(error.message); });
|
因此下面的同步 try/catch 捕获不到该拒绝:
1 2 3 4 5
| try { getUser(); } catch (error) { }
|
正确方式是调用 .catch(),或者在另一个 async 函数中用 await 和 try/catch。
六、await 的含义
await 会等待一个 Promise 完成:
1 2 3 4
| async function main() { const value = await Promise.resolve(10); console.log(value); }
|
- Promise fulfilled:
await 表达式得到成功值。
- Promise rejected:
await 会在当前位置抛出拒绝原因。
1 2 3
| async function main() { const result = await Promise.reject(new Error("请求失败")); }
|
这里的失败会使 main() 返回一个 rejected Promise。若要在当前函数处理错误,应使用 try/catch:
1 2 3 4 5 6 7 8 9 10
| async function loadUser() { try { const response = await fetch("/api/user"); const user = await response.json(); return user; } catch (error) { console.error("加载用户失败:", error); return null; } }
|
如果其中任何一个被等待的 Promise 失败,之后的语句都会被跳过,控制流直接进入 catch:
1 2 3 4 5 6 7 8 9 10
| async function run() { try { const a = await stepA(); const b = await stepB(a); const c = await stepC(b); return c; } catch (error) { console.error("某一步失败:", error); } }
|
它与下面的 Promise 链具有相同的错误传播结构:
1 2 3 4
| stepA() .then(stepB) .then(stepC) .catch(error => console.error(error));
|
七、async/await 中的错误向上传播
如果没有 try/catch,或者捕获后重新抛出,错误会使当前 async 函数返回 rejected Promise:
1 2 3 4 5 6 7 8 9 10 11 12 13
| async function getProfile() { const user = await getUser(); return getUserProfile(user.id); }
async function main() { try { const profile = await getProfile(); console.log(profile); } catch (error) { console.error("最终捕获:", error); } }
|
传播过程如下:
1 2 3 4 5 6 7 8 9
| 底层 Promise rejected ↓ await 在当前位置抛出错误 ↓ getProfile 返回 rejected Promise ↓ main 中的 await 再次抛出 ↓ main 的 catch 捕获
|
捕获并恢复
1 2 3 4 5 6 7 8
| async function getConfig() { try { return await requestConfig(); } catch (error) { console.error("配置请求失败:", error); return { theme: "light" }; } }
|
因为 catch 返回了普通值,getConfig() 最终是 fulfilled。
捕获后继续传播
1 2 3 4 5 6 7 8
| async function getConfig() { try { return await requestConfig(); } catch (error) { console.error("记录错误:", error); throw error; } }
|
包装错误并保留原始原因
1 2 3 4 5 6 7 8 9
| async function getUser(id) { try { return await database.findUser(id); } catch (error) { throw new Error(`查询用户 ${id} 失败`, { cause: error }); } }
|
这样既能增加业务上下文,也能通过 error.cause 保留底层错误。
八、return promise 与 return await
通常下面两种写法最终结果相近:
1 2 3 4 5 6 7
| async function getData() { return requestData(); }
async function getData() { return await requestData(); }
|
但在当前函数的 try/catch 中,两者有重要区别:
1 2 3 4 5 6 7 8
| async function getData() { try { return requestData(); } catch (error) { return null; } }
|
使用 return await,拒绝会在当前 try 中被转换成抛出,因此可以被当前 catch 捕获:
1 2 3 4 5 6 7 8
| async function getData() { try { return await requestData(); } catch (error) { console.error("请求失败:", error); return null; } }
|
经验规则:
- 只是把 Promise 交给调用者处理:可以
return promise。
- 需要在当前
try/catch 中处理失败:使用 return await promise。
九、HTTP 失败不一定等于 Promise 失败
fetch() 遇到网络错误或请求被中断时会 reject,但 HTTP 404、500 通常仍然会返回 fulfilled Promise。因此要主动检查状态码:
1 2 3 4 5 6 7 8 9 10 11
| async function requestUser() { const response = await fetch("/api/user");
if (!response.ok) { throw new Error( `请求失败:${response.status} ${response.statusText}` ); }
return response.json(); }
|
这说明“技术上的 Promise 成功”和“业务操作成功”并不总是同一件事,业务失败往往需要程序主动判断并抛出。
十、并发 Promise 的错误传播
Promise.all
等待全部 Promise 成功;任何一个失败,整体就会 reject:
1 2 3 4 5 6 7 8 9 10 11 12 13
| async function loadPage() { try { const [user, articles, messages] = await Promise.all([ getUser(), getArticles(), getMessages() ]);
return { user, articles, messages }; } catch (error) { console.error("至少一个请求失败:", error); } }
|
注意:Promise.all() 失败并不意味着其他操作会自动取消,它们通常仍会继续运行。
Promise.allSettled
等待全部 Promise 完成,并分别报告成功或失败:
1 2 3 4 5 6
| const results = await Promise.allSettled([ Promise.resolve("A"), Promise.reject(new Error("B 失败")) ]);
console.log(results);
|
结果类似:
1 2 3 4
| [ { status: "fulfilled", value: "A" }, { status: "rejected", reason: Error("B 失败") } ]
|
适合所有任务都必须完成,并需要分别统计结果的场景。
Promise.any
只要有一个成功,整体就成功;全部失败时抛出 AggregateError:
1 2 3 4 5 6 7 8 9 10 11
| try { const result = await Promise.any([ requestServerA(), requestServerB(), requestServerC() ]); console.log(result); } catch (error) { console.error(error instanceof AggregateError); console.error(error.errors); }
|
Promise.race
哪个 Promise 最先结束,就采用哪个结果,无论成功还是失败:
1 2 3 4 5 6 7 8 9 10 11 12 13
| const timeout = new Promise((_, reject) => { setTimeout(() => reject(new Error("请求超时")), 3000); });
try { const result = await Promise.race([ requestData(), timeout ]); console.log(result); } catch (error) { console.error(error.message); }
|
race() 只决定采用谁的结果,并不会自动取消其他任务。
十一、串行和并行 await
下面是串行执行,第二个任务要等待第一个完成后才开始:
1 2
| const user = await getUser(); const articles = await getArticles();
|
如果两者互不依赖,应使用并行方式:
1 2 3 4
| const [user, articles] = await Promise.all([ getUser(), getArticles() ]);
|
并行可以减少总等待时间,并让错误处理集中在同一个 try/catch 中。
十二、常见错误写法
1. 忘记 await
1 2 3 4 5 6 7 8
| async function main() { try { const user = getUser(); console.log(user.name); } catch (error) { console.error(error); } }
|
应改为:
1
| const user = await getUser();
|
2. 忘记返回 Promise
1 2 3 4 5
| function getUser() { fetch("/api/user").then(response => response.json()); }
getUser().then(console.log);
|
正确写法:
1 2 3
| function getUser() { return fetch("/api/user").then(response => response.json()); }
|
3. 在 then 中忘记 return
1 2 3 4 5 6 7
| fetch("/api/user") .then(response => { response.json(); }) .then(user => { console.log(user); });
|
正确写法:
1 2 3
| fetch("/api/user") .then(response => response.json()) .then(console.log);
|
4. 捕获后无意中吞掉错误
1 2 3 4 5 6 7
| async function saveData() { try { await database.save(); } catch (error) { console.log(error); } }
|
由于 catch 正常结束,调用者会认为 saveData() 已成功完成。如果调用者必须知道失败,应重新抛出:
1 2 3 4 5 6 7 8
| async function saveData() { try { await database.save(); } catch (error) { console.error("保存失败:", error); throw error; } }
|
5. forEach 不会等待 async 回调
1 2 3 4 5
| users.forEach(async user => { await saveUser(user); });
console.log("全部完成");
|
需要串行时:
1 2 3
| for (const user of users) { await saveUser(user); }
|
需要并行时:
1
| await Promise.all(users.map(user => saveUser(user)));
|
十三、未处理的 Promise rejection
如果 rejected Promise 没有被处理,就会产生未处理拒绝:
1 2 3 4 5
| async function run() { throw new Error("失败"); }
run();
|
正确做法:
1 2 3
| run().catch(error => { console.error(error); });
|
对于明确不等待结果的任务,也应处理其错误:
1 2 3
| void sendAnalytics().catch(error => { console.error("统计上报失败:", error); });
|
void 表明调用者明确放弃结果,而 .catch() 负责避免未处理拒绝。
十四、完整的分层错误处理示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| class HttpError extends Error { constructor(status, message, options) { super(message, options); this.name = "HttpError"; this.status = status; } }
async function requestJson(url) { let response;
try { response = await fetch(url); } catch (error) { throw new Error("无法连接服务器", { cause: error }); }
if (!response.ok) { throw new HttpError( response.status, `HTTP 请求失败:${response.status}` ); }
try { return await response.json(); } catch (error) { throw new Error("服务器返回了无效 JSON", { cause: error }); } }
async function getUser(id) { try { return await requestJson(`/api/users/${id}`); } catch (error) { throw new Error(`获取用户 ${id} 失败`, { cause: error }); } }
async function renderUserPage(id) { try { const user = await getUser(id); console.log("渲染用户:", user); } catch (error) { console.error("页面加载失败:", error.message);
let current = error.cause; while (current) { console.error("原始原因:", current.message); current = current.cause; }
console.log("显示错误提示页面"); } finally { console.log("关闭加载动画"); } }
renderUserPage(42).catch(error => { console.error("未处理的顶层错误:", error); });
|
错误传播路径可能是:
1 2 3 4 5 6 7 8 9
| fetch 网络失败 ↓ requestJson 转换为“无法连接服务器” ↓ getUser 补充“获取用户 42 失败”的业务上下文 ↓ renderUserPage 捕获并展示错误页面 ↓ finally 关闭加载动画
|
推荐的分层原则是:
- 底层产生或转换技术错误。
- 中间层补充业务上下文。
- 最上层决定如何展示、重试或终止。
- 不要在每一层都无意义地捕获并吞掉错误。
十五、Promise 与 async/await 对照
| Promise 写法 |
async/await 写法 |
Promise.resolve(value) |
async 函数中 return value |
Promise.reject(error) |
async 函数中 throw error |
.then(value => ...) |
const value = await ... |
.catch(error => ...) |
try/catch |
.finally(() => ...) |
try/finally |
| 回调返回普通值 |
当前链恢复为 fulfilled |
| 回调抛出异常 |
当前链变成 rejected |
| 回调返回 Promise |
等待并采用它的最终状态 |
结论
Promise 是 JavaScript 异步结果的基础表示方式,async/await 是建立在 Promise 之上的控制流语法。最核心的规则是:
- async 函数一定返回 Promise。
- async 函数中的
return value 会产生 fulfilled Promise。
- async 函数中的
throw error 会产生 rejected Promise。
await 遇到 fulfilled Promise,会得到其成功值。
await 遇到 rejected Promise,会在当前位置抛出错误。
- Promise 链中的异常会跳过后续成功处理器,传播到最近的
.catch()。
.catch() 或 catch 返回普通值意味着错误已经恢复;重新 throw 才会继续传播。
try/catch 只能捕获真正被 await 的异步失败,不能自动捕获一个未等待的 Promise。
- 多个独立异步任务应优先用
Promise.all() 并发执行;需要收集全部结果时使用 Promise.allSettled()。
- 每条可能失败的异步调用链,最终都应该有明确的错误处理边界,避免未处理的 Promise rejection。
实际开发中,可以优先使用 async/await 编写主要业务流程,用 Promise.all() 等 Promise API 表达并发关系,并按照“底层抛出、中层补充上下文、顶层统一处理”的方式组织错误。