1. 引言:为什么要掌握 Promise 的错误处理
在 JavaScript 异步编程中,Promise 早已成为处理异步任务的标准方案。无论是网络请求、文件读写,还是定时器、事件回调,几乎都离不开 Promise。然而,很多开发者在初学阶段会遇到两类高频问题:
.then()的第二个参数和.catch()到底有什么区别?- 如何把一个基于回调的异步 API 正确封装成 Promise?
这两个问题看似基础,却直接决定了代码的健壮性:错误能否被正确捕获、会不会出现 Unhandled Rejection、调用方拿到的是真 Promise 还是 undefined。本篇文章将通过可运行的示例代码,一步步拆解这两个问题,并给出最佳的封装实践。
2. Promise 核心概念速览
在深入之前,先快速回顾 Promise 的几个核心要点:
- Promise 有三种状态:
pending(进行中)、fulfilled(已成功)、rejected(已失败)。 - 状态一旦从
pending变为fulfilled或rejected,就不可逆转。 then()用于注册成功和失败的回调;catch()用于捕获失败;finally()无论成功失败都会执行。- 每个
then()/catch()都会返回一个新的 Promise,因此可以链式调用。
先看一个最基础的例子:
javascript
const p = Promise.resolve('hello');
p.then(
(val) => console.log('fulfilled:', val),
(err) => console.log('rejected:', err)
);
console.log('after then');
// 输出结果
// after then
// fulfilled: hello
可以看到,then() 里的回调是异步执行 的【进微队列,等正在执行的代码执行完,才会从取微队列中的任务执行】,所以 after then 先打印,随后才是 fulfilled: hello。
3. .then() 第二个参数 vs .catch():谁能捕获错误
3.1 基本用法回顾
then() 方法最多接收两个参数:
javascript
promise.then(onFulfilled, onRejected);
onFulfilled:Promise 成功时调用。onRejected:Promise 失败时调用。
而 catch() 相当于只传了失败回调的 then:
javascript
promise.catch(onRejected);
// 等价于
promise.then(undefined, onRejected);
3.2 关键差异:能捕获的错误范围不同
这是很多开发者容易踩坑的地方。请看下面两组对比代码:
javascript
// 场景 A:使用 .then 的第二个参数
promise
.then(
(res) => {
throw new Error('onFulfilled 里抛出的错误');
},
(err) => {
console.log('能捕获到吗?'); // ❌ 捕获不到!
// 这个 err 只能捕获 promise 本身的 reject
// 捕获不到 onFulfilled 里抛出的错误
}
);
// 场景 B:使用 .catch
promise
.then((res) => {
throw new Error('onFulfilled 里抛出的错误');
})
.catch((err) => {
console.log('能捕获到吗?'); // ✅ 能!包括前面整条链的错误
});
核心结论:
.then(onFulfilled, onRejected)中的onRejected只能捕获当前 Promise 的 reject ,无法捕获onFulfilled回调里抛出的错误。.catch()放在链尾,可以捕获前面整条链上任何一环抛出的错误 ,包括then回调中的异常、以及中间 Promise 的 reject。
因此,在实际项目中更推荐使用 .catch() 来统一处理错误,而不是依赖 then() 的第二个参数。
4. 封装回调函数为 Promise:常见错误
很多基于 Node.js 回调风格(callback(err, data))的 API,例如 fs.readFile、fs.writeFile、数据库驱动等,使用时需要手动封装成 Promise。封装过程中最常见的错误,是没有真正返回 Promise。
4.1 一个典型的错误封装
javascript
// 错误代码
const readFileAsync = (path: string): Promise<string> => {
fs.readFile(path, 'utf-8', (err, data) => {
if (err) {
return Promise.reject(err); // ← 问题在这里
}
});
// ← 这里没有 return!函数实际返回 undefined
};
这段代码存在两个明显问题:
错误 1:函数根本没有返回 Promise
函数体末尾没有 return,所以 readFileAsync() 实际返回值是 undefined。类型签名却声明为 Promise<string>,这等于在类型上"欺骗"了调用者。调用方拿到 undefined 后继续调用 .then(),会直接抛出类型错误。
错误 2:Promise.reject(err) 返回给了谁?
先看 fs.readFile 的标准签名:
javascript
// fs.readFile 签名
fs.readFile(
path: string,
encoding: string,
callback: (err: Error | null, data: string) => void
): void
fs.readFile 本身返回 void,callback 是它内部调用的。在回调里写:
javascript
return Promise.reject(err);
这个 return 只是从 callback 函数 返回,创建出的 rejected Promise 对象没有任何人接住 ,于是变成了 Unhandled Rejection。外部调用者依然拿到 undefined。
4.2 错误代码的实际执行流程
错误代码的实际执行流程:
readFileAsync()没有return→ 返回undefinedfs.readFile读取文件(内部触发,跟 Promise 无关)- 回调里
return Promise.reject(err)- 这个 Promise 没人接收 → 变成 Unhandled Rejection
- 调用者拿到
undefined→.then()直接报错
可以看到,错误的根源在于:没有把回调逻辑包裹进 Promise,也没有把 Promise 返回出去。
5. 正确的封装方式
5.1 使用 new Promise 包裹回调
正确做法是:在外层显式 return new Promise(...),在回调内部通过 resolve / reject 改变 Promise 状态:
javascript
const readFileAsync = (path: string): Promise<string> => {
return new Promise((resolve, reject) => { // ← 创建一个 Promise
fs.readFile(path, 'utf-8', (err, data) => {
if (err) {
reject(err); // ← 调用 reject 回调,改变 Promise 的状态
return;
}
resolve(data); // ← 成功时改变 Promise 的状态
});
});
};
这样修改后:
- 函数真正返回一个
Promise<string>。 - 文件读取成功时,
resolve(data)使 Promise 进入fulfilled状态。 - 读取失败时,
reject(err)使 Promise 进入rejected状态,并且这个状态被返回给调用方,不会出现 Unhandled Rejection。 - 调用方可以链式使用
.then()/.catch()。
5.2 使用 async/await 调用
封装完成后,调用方可以用现代语法优雅地消费:
javascript
const main = async () => {
try {
const content = await readFileAsync('/path/to/file');
console.log(content);
} catch (err) {
console.error('读取失败:', err);
}
};
async/await 让异步代码看起来像同步代码,同时配合 try/catch 可以清晰捕获错误。这里再次体现了 Promise 的价值:可组合、可传递、可被 await。
6. 错误处理最佳实践
结合前文内容,总结几条 Promise 错误处理的实用建议:
- 优先使用
.catch():把错误处理集中在链尾,避免依赖.then()第二参数漏掉回调内的异常。 - 封装回调 API 时,必须
return new Promise(...):把callback(err, data)内部的err/data正确映射到reject/resolve。 - 回调里调用
reject后记得return:避免继续执行后续逻辑造成意外状态变更。 - 使用
async/await时始终配合try/catch:否则异步错误可能穿透到全局,难以追踪。 - 避免吞掉错误 :不要写空的
catch,至少记录日志或抛出提示,否则会让问题难以排查。
7. 总结
本文围绕 Promise 的两个高频痛点展开:
.then()的第二个参数只能捕获当前 Promise 的 reject,而.catch()可以捕获整条链上的错误,因此更推荐后者。- 封装回调风格的异步 API 时,必须显式返回
new Promise,并在回调内正确调用resolve/reject,否则会出现返回undefined、Unhandled Rejection 等隐蔽问题。
掌握这两点,能帮助你写出更健壮、更易维护的异步代码。Promise 虽然已经出现多年,但它依然是 JavaScript 异步编程的基石,理解其错误传播机制,对后续学习 async/await、微任务队列乃至更高级的异步模式都大有裨益。
#mermaid-svg-3kBOO4t0i0uchSlP{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-3kBOO4t0i0uchSlP .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-3kBOO4t0i0uchSlP .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-3kBOO4t0i0uchSlP .error-icon{fill:#552222;}#mermaid-svg-3kBOO4t0i0uchSlP .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-3kBOO4t0i0uchSlP .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-3kBOO4t0i0uchSlP .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-3kBOO4t0i0uchSlP .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-3kBOO4t0i0uchSlP .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-3kBOO4t0i0uchSlP .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-3kBOO4t0i0uchSlP .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-3kBOO4t0i0uchSlP .marker{fill:#333333;stroke:#333333;}#mermaid-svg-3kBOO4t0i0uchSlP .marker.cross{stroke:#333333;}#mermaid-svg-3kBOO4t0i0uchSlP svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-3kBOO4t0i0uchSlP p{margin:0;}#mermaid-svg-3kBOO4t0i0uchSlP .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-3kBOO4t0i0uchSlP .cluster-label text{fill:#333;}#mermaid-svg-3kBOO4t0i0uchSlP .cluster-label span{color:#333;}#mermaid-svg-3kBOO4t0i0uchSlP .cluster-label span p{background-color:transparent;}#mermaid-svg-3kBOO4t0i0uchSlP .label text,#mermaid-svg-3kBOO4t0i0uchSlP span{fill:#333;color:#333;}#mermaid-svg-3kBOO4t0i0uchSlP .node rect,#mermaid-svg-3kBOO4t0i0uchSlP .node circle,#mermaid-svg-3kBOO4t0i0uchSlP .node ellipse,#mermaid-svg-3kBOO4t0i0uchSlP .node polygon,#mermaid-svg-3kBOO4t0i0uchSlP .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-3kBOO4t0i0uchSlP .rough-node .label text,#mermaid-svg-3kBOO4t0i0uchSlP .node .label text,#mermaid-svg-3kBOO4t0i0uchSlP .image-shape .label,#mermaid-svg-3kBOO4t0i0uchSlP .icon-shape .label{text-anchor:middle;}#mermaid-svg-3kBOO4t0i0uchSlP .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-3kBOO4t0i0uchSlP .rough-node .label,#mermaid-svg-3kBOO4t0i0uchSlP .node .label,#mermaid-svg-3kBOO4t0i0uchSlP .image-shape .label,#mermaid-svg-3kBOO4t0i0uchSlP .icon-shape .label{text-align:center;}#mermaid-svg-3kBOO4t0i0uchSlP .node.clickable{cursor:pointer;}#mermaid-svg-3kBOO4t0i0uchSlP .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-3kBOO4t0i0uchSlP .arrowheadPath{fill:#333333;}#mermaid-svg-3kBOO4t0i0uchSlP .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-3kBOO4t0i0uchSlP .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-3kBOO4t0i0uchSlP .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3kBOO4t0i0uchSlP .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-3kBOO4t0i0uchSlP .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3kBOO4t0i0uchSlP .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-3kBOO4t0i0uchSlP .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-3kBOO4t0i0uchSlP .cluster text{fill:#333;}#mermaid-svg-3kBOO4t0i0uchSlP .cluster span{color:#333;}#mermaid-svg-3kBOO4t0i0uchSlP div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-3kBOO4t0i0uchSlP .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-3kBOO4t0i0uchSlP rect.text{fill:none;stroke-width:0;}#mermaid-svg-3kBOO4t0i0uchSlP .icon-shape,#mermaid-svg-3kBOO4t0i0uchSlP .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3kBOO4t0i0uchSlP .icon-shape p,#mermaid-svg-3kBOO4t0i0uchSlP .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-3kBOO4t0i0uchSlP .icon-shape .label rect,#mermaid-svg-3kBOO4t0i0uchSlP .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3kBOO4t0i0uchSlP .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-3kBOO4t0i0uchSlP .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-3kBOO4t0i0uchSlP :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
存在
不存在
封装回调 API
是否 return new Promise?
返回 undefined
调用方 .then() 报错
回调中 err 是否存在?
reject(err)
resolve(data)
进入 .catch / try-catch
进入 .then / await