Promise 基础与进阶:状态规则、链式错误处理和异步执行顺序

Promise 最难的部分不是记住 then 的语法,而是看清"哪个 Promise 已经确定结果""错误会沿哪一段链传播""回调何时执行"。掌握这三件事,就能读懂链式调用、Promise.all 和 async/await。本文示例均可在现代浏览器控制台或 Node.js 中运行。

一 从基础开始 Promise 代表什么

Promise 是一个异步操作结果的占位对象,有三种状态:pending(待定)、fulfilled(成功)和 rejected(失败)。成功保存一个值,失败保存一个原因。

javascript 复制代码
const p = new Promise((resolve, reject) => {
  setTimeout(() => resolve("数据已返回"), 100);
});

p.then(
  value => console.log(value),
  reason => console.error(reason)
);

构造器里的执行器函数会立即同步执行 ;then 注册的处理函数会在相应结果确定后进入微任务队列,不会在当前同步代码中直接执行。

二 三条必须记住的规则

规则一 状态只能确定一次

Promise 一旦从 pending 走向 fulfilled 或 rejected,后续对它的 resolve、reject 调用不再改变结果。

javascript 复制代码
const p = new Promise((resolve, reject) => {
  resolve("第一次");
  reject(new Error("太晚了"));
  resolve("第二次");
});

p.then(console.log); // 第一次

更精确地说,调用 resolve(另一个 Promise) 时,外层 Promise 已经"锁定"到那个 Promise 的最终结果;它此时仍可能显示为 pending,但后续 reject 也无法抢先改写结果。

javascript 复制代码
let finish;
const inner = new Promise(resolve => { finish = resolve; });
const outer = new Promise((resolve, reject) => {
  resolve(inner);                 // 结果锁定到 inner
  reject(new Error("无效"));      // 不会生效
});
outer.then(console.log);
finish("最终值");                 // 最终值

规则二 throw 会把当前链节变成失败

执行器或 then 回调中抛出的异常,会使对应的新 Promise 变为 rejected。但异步定时器中抛出的异常,不在构造器同步捕获范围内,不能靠外层 Promise 自动接住。

typescript 复制代码
Promise.resolve(1)
  .then(() => { throw new Error("计算失败"); })
  .catch(error => console.log(error.message)); // 计算失败

throw new Error(...) 与在 then 回调中 return Promise.reject(new Error(...)),都能让这一步返回的 Promise 失败。推荐用 Error 对象保留堆栈信息。

规则三 resolve 会展开嵌套 reject 不会

resolve 接收 Promise 或 thenable 时,会跟随其最终状态和值;reject 则直接把传入内容当作失败原因。

typescript 复制代码
const inner = Promise.resolve(42);

new Promise(resolve => resolve(inner))
  .then(value => console.log(value));          // 42

new Promise((_, reject) => reject(inner))
  .catch(reason => console.log(reason === inner)); // true

所以"状态锁定"与"状态已成功"不是一回事:resolve(inner) 可能最终失败,取决于 inner。

三 静态工厂方法 Promise.resolve 与 Promise.reject

Promise.resolve(value) 创建一个最终遵循 value 的 Promise。普通值会成为成功值;Promise 或 thenable 会被展开。传入同一个原生 Promise 时,通常直接返回该 Promise。

javascript 复制代码
Promise.resolve(7).then(console.log);                // 7
Promise.resolve(Promise.resolve(8)).then(console.log); // 8

Promise.reject(reason) 创建失败的 Promise,不会展开 reason;即使原因本身是 Promise,也原样保留。

typescript 复制代码
const reason = Promise.resolve("内部成功");
Promise.reject(reason)
  .catch(error => console.log(error === reason)); // true

这两个方法都立即返回 Promise,但 then / catch 回调仍在后续微任务中执行。"同步返回 Promise"不等于"同步执行回调"。Promise.reject(new Error("请求失败")) 可以构造一个失败结果;实际代码里应及时附上错误处理,避免未处理的拒绝。

四 从回调地狱到可组合的链

假设要依次读取用户、订单和详情。层层嵌套时,每一步的成功与失败分支会不断加深:

scss 复制代码
getUser(userId, (error, user) => {
  if (error) return handleError(error);
  getOrders(user.id, (error, orders) => {
    if (error) return handleError(error);
    getDetail(orders[0].id, (error, detail) => {
      if (error) return handleError(error);
      render(detail);
    });
  });
});

把每个 API 包装成返回 Promise 的函数后,顺序流程可以写成一条链:

ini 复制代码
getUser(userId)
  .then(user => getOrders(user.id))
  .then(orders => getDetail(orders[0].id))
  .then(detail => render(detail))
  .catch(handleError);

每次 then 都返回一个新的 Promise。回调返回普通值,下一步收到该值;返回 Promise,下一步等待它;抛错或返回失败 Promise,后续成功回调会跳过,直到遇到错误处理。

别漏写 return,否则下一步会过早执行:

ini 复制代码
getUser(userId)
  .then(user => {
    return getOrders(user.id); // 把异步结果交给下一步
  })
  .then(orders => console.log(orders));

五 then 的第二个参数与 catch 的区别

then(onFulfilled, onRejected) 的第二个参数处理的是进入这个 then 时 已有的失败;catch(onRejected) 等价于 then(undefined, onRejected),但放在链尾时能接住前面整段链传下来的失败。

typescript 复制代码
Promise.resolve("ok")
  .then(
    () => { throw new Error("成功回调出错"); },
    error => console.log("第二参数", error)
  )
  .catch(error => console.log("链尾 catch", error.message));
// 链尾 catch 成功回调出错

同一个 then 的第二参数接不住第一个参数自己抛出的异常,因为异常发生在这个 then 创建的新 Promise 上。它也接不住自己再次抛出的异常;后续 catch 可以。

错误处理函数若正常返回,链会恢复为成功状态:

typescript 复制代码
Promise.reject(new Error("网络失败"))
  .catch(() => "缓存数据")
  .then(value => console.log(value)); // 缓存数据

若处理后仍需让调用方知道失败,应重新 throw 该错误。

六 微任务 宏任务和执行顺序

Promise 的 then / catch / finally 回调属于微任务;setTimeout 回调属于任务队列中的后续任务,常被称为宏任务。当前同步代码先执行完,再清空微任务队列,之后才轮到定时器任务。

javascript 复制代码
console.log("A");

setTimeout(() => console.log("D"), 0);

Promise.resolve()
  .then(() => {
    console.log("B");
    queueMicrotask(() => console.log("C"));
  });

console.log("E");
// A E B C D

Promise.resolve() 本身不等待;它立即返回一个已成功的 Promise,但 then 回调仍异步排队。分析顺序时,先列同步输出,再按入队先后处理微任务,最后处理定时器等后续任务。不同运行环境的事件循环细节可能不同,这个例子在现代浏览器和 Node.js 中结果一致。

七 Promise.all 处理并行任务

Promise.all(iterable) 等待一组 Promise 全部成功,成功结果按输入顺序组成数组,与完成先后无关;只要有一个失败,返回的 Promise 就会失败。普通值也能作为输入。

scss 复制代码
const [user, settings] = await Promise.all([
  fetchUser(),
  fetchSettings()
]);

独立任务适合并行启动;存在依赖时,应先等待前一步的结果。Promise.all 的"快速失败"不会自动取消其他已启动任务。若需要取消请求,可按具体 API 使用 AbortController。

javascript 复制代码
Promise.all([
  Promise.resolve("A"),
  2,
  new Promise(resolve => setTimeout(() => resolve("C"), 10))
]).then(console.log); // ["A", 2, "C"]

空数组 Promise.all([]) 会得到成功值 [],但附加的 then 回调仍是微任务。

八 async await 是 Promise 的另一种写法

async 函数总是返回 Promise;return value 对应成功,throw error 对应失败。await 等待 Promise 的结果:成功时得到值,失败时抛出原因,因而可以用 try/catch 处理。

vbnet 复制代码
async function loadPage(userId) {
  try {
    const user = await getUser(userId);
    const orders = await getOrders(user.id);
    return await getDetail(orders[0].id);
  } catch (error) {
    console.error("加载失败", error);
    throw error; // 交给调用方继续处理
  }
}

独立任务不要无意中串行等待:

scss 复制代码
// 并行启动并等待两项结果
const [profile, notices] = await Promise.all([
  getProfile(),
  getNotices()
]);

await 之后的代码会在后续微任务中继续执行;它不会阻塞整个 JavaScript 线程。

九 一张表复盘

写法 结果
resolve(普通值) 成功,值就是该普通值
resolve(Promise) 跟随该 Promise 的最终结果
reject(任意原因) 失败,原因原样保留
then 回调返回普通值 下一步成功
then 回调返回 Promise 下一步等待它
then 回调抛错 下一步失败
catch 正常返回 链恢复为成功
catch 再次抛错 失败继续向后传播

读一段 Promise 代码时,逐步问自己:这一行返回的是哪个新 Promise?它是返回值、等待另一个 Promise,还是抛错?当前同步代码结束后,哪些回调会进入微任务队列?这三个问题比背执行顺序口诀更可靠。

相关推荐
代码猎人1 小时前
前端怎么搭建项目基础架构
前端
Amos_Web1 小时前
Rspack 源码解析(二十五):CssParserAndGenerator 如何把 CSS 变成可 Tree Shaking 的模块
前端·rust·前端框架
wespaul1 小时前
VeapAI 自然语言操作实战|第 06 篇:AiConsole 前端运行时——一个 Hub,八个注册表
前端·人工智能·milvus
龙亘川2 小时前
健康服务规范化背后,数字化如何 “辅助赋能“
大数据·前端·人工智能·智慧城市·数据可视化
data analyse 4562 小时前
SourceMap 上传版本与报错版本对不上?版本校验与错误还原设计
大数据·前端·人工智能·数据分析
前端双越老师2 小时前
程序员 1v1 访谈系列第三期:先不要想在工作中有突破
前端·ai编程·全栈
小羊没烦恼!2 小时前
浅谈SQL Server中的事务日志(二)----事务日志在修改数据时的角色
前端·数据库·后端·python·web安全·oracle
myaifas2 小时前
新人的第一个任务该怎么选
前端·产品经理
怕浪猫2 小时前
前端转全栈最低成本的技术栈选择
前端·javascript·面试