JavaScript Promise 实战:.then() 与 .catch() 的区别及回调函数封装指南

1. 引言:为什么要掌握 Promise 的错误处理

在 JavaScript 异步编程中,Promise 早已成为处理异步任务的标准方案。无论是网络请求、文件读写,还是定时器、事件回调,几乎都离不开 Promise。然而,很多开发者在初学阶段会遇到两类高频问题:

  1. .then() 的第二个参数和 .catch() 到底有什么区别?
  2. 如何把一个基于回调的异步 API 正确封装成 Promise?

这两个问题看似基础,却直接决定了代码的健壮性:错误能否被正确捕获、会不会出现 Unhandled Rejection、调用方拿到的是真 Promise 还是 undefined。本篇文章将通过可运行的示例代码,一步步拆解这两个问题,并给出最佳的封装实践。

2. Promise 核心概念速览

在深入之前,先快速回顾 Promise 的几个核心要点:

  • Promise 有三种状态:pending(进行中)、fulfilled(已成功)、rejected(已失败)。
  • 状态一旦从 pending 变为 fulfilledrejected,就不可逆转
  • 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.readFilefs.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 本身返回 voidcallback 是它内部调用的。在回调里写:

javascript 复制代码
return Promise.reject(err);

这个 return 只是从 callback 函数 返回,创建出的 rejected Promise 对象没有任何人接住 ,于是变成了 Unhandled Rejection。外部调用者依然拿到 undefined

4.2 错误代码的实际执行流程

错误代码的实际执行流程:

  1. readFileAsync() 没有 return → 返回 undefined
  2. fs.readFile 读取文件(内部触发,跟 Promise 无关)
  3. 回调里 return Promise.reject(err)
  4. 这个 Promise 没人接收 → 变成 Unhandled Rejection
  5. 调用者拿到 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

相关推荐
大模型码小白1 小时前
数据可视化:AI处理多维数据的HTML5可视化方案
大数据·前端·javascript·人工智能·机器学习·信息可视化·html5
名字还没想好☜1 小时前
Go 1.21 context.WithoutCancel 实战:父 context 取消了,收尾任务还要继续跑
开发语言·后端·golang·go
wdfk_prog1 小时前
canopennode-rtt推荐,不只可以做从站,也可以承担主站角色
c语言·开发语言·数据库·学习·算法·深度优先
启观川1 小时前
Python基础-第 16 章 综合案例:客户信息管理系统
开发语言·笔记·python·正则表达式
无小道2 小时前
C/C++——可变参数
c语言·开发语言·c++·可变参数
BioRunYiXue2 小时前
科研干货 | IC50全面解读:概念解析、实验设计与数据分析要点
java·开发语言·javascript·人工智能·算法·数据挖掘·数据分析
边境悍匪2 小时前
蜗牛学苑 Java 智能体学习 Day34|AOP 进阶、权限思维导图复盘
java·开发语言·spring boot·学习
每天吃饭的羊2 小时前
WebAssembly
开发语言·javascript·wasm
@福者2 小时前
C++ QT 入门
开发语言·c++·qt