JavaScript Promise 解释:then、catch、Promise.all 和常见错误

  1. 首页
  2. 博客
  3. 软件开发

软件开发

JavaScript Promise 解释:then、catch、Promise.all 和常见错误

Promise 是 JavaScript 处理稍后完成的工作的方式,而 async/await 是建立在它们之上的。通过每个示例的真实输出来了解它们的真实行为,并避免错误吞噬您的错误。

阅读约 10 分钟
JavaScript Promise 解释:then、catch、Promise.all 和常见错误

要点速览

  • promise 是一个表示稍后可用的结果的对象。
  • 它会一直等待,直到满足或拒绝为止,并且一旦解决就永远不会改变。
  • 每个 then 返回一个新的 Promise,这使得链接成为可能。
  • 当需要每个结果时使用 Promise.all,当您想要每个结果时使用 Promise.allSettled。

网络请求、文件读取和计时器都会稍后完成,但 JavaScript 不能简单地停止和等待,否则页面会冻结。 JavaScript Promise 为您提供了一种简洁的方式来表达“当此操作完成时,执行此操作,如果失败,则执行此操作”。

即使您主要编写 async 和 await,您也会在下面使用 Promise,因此理解它们会使调试变得更加容易。我们在 Node.js 24 上运行这些示例,显示的输出来自该运行。它们在现代浏览器中的行为方式相同。

什么是承诺?

承诺是未来值的占位符。想象一下当你点餐时餐馆递给你的蜂鸣器。你还没有拿到食物,但你拿着一些东西,它会告诉你食物何时准备好,或者有问题,同时你可以自由地做其他事情。

在代码中,异步函数立即返回一个承诺,然后您附加接下来应该发生的事情:

fetch("/api/user")
  .then((response) => response.json())
  .then((user) => console.log(user.name))
  .catch((error) => console.log("Failed:", error.message));

三个承诺状态

状态 含义 运行什么
待定 工作还没有完成 还没有什么
实现了 它以一个值结束 then 处理程序
被拒绝 失败是有原因的 catch 处理程序

履行或拒绝的承诺称为已解决。承诺只能解决一次。之后,它的状态和值就固定了,以后对 resolve 或 reject 的调用将被忽略。

创造一个承诺

您通常会使用 fetch 等 API 中的 Promise,但创建一个 Promise 会显示它们是如何工作的。构造函数接收两个函数,resolve 和 reject:

function fetchUser(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id > 0) {
        resolve({ id, name: "Asha" });
      } else {
        reject(new Error("Invalid id"));
      }
    }, 100);
  });
}

console.log(fetchUser(1));

// Promise { <pending> }

立即记录承诺表明它仍处于待处理状态,因为计时器尚未触发。该值只能通过处理程序或 await 访问。始终使用 Error 对象而不是字符串来拒绝,因为 Error 带有有助于调试的堆栈跟踪。

然后,抓住,最后

这三个方法将处理程序附加到 Promise:

fetchUser(1).then((user) => console.log(user.name));
// Asha

fetchUser(-1)
  .then((user) => console.log("never runs", user))
  .catch((error) => console.log("caught:", error.message))
  .finally(() => console.log("finally"));

// caught: Invalid id
// finally

then 成功时运行,catch 失败时运行,finally 两种情况都运行。这使得 finally 成为清理的正确位置,例如隐藏加载旋转器。它不会收到任何争论,因为它不应该关心结果。

连锁承诺

关键思想是 then 总是返回一个 新的 Promise。无论你的处理程序返回什么,都会成为新承诺的值:

Promise.resolve(2)
  .then((n) => n * 10)
  .then((n) => n + 1)
  .then((n) => console.log("chain:", n));

// chain: 21

如果处理程序返回另一个 Promise,链会等待它稳定下来,然后再继续。这就是将嵌套回调转变为扁平步骤序列的原因:

getUser(id)
  .then((user) => getOrders(user.id))
  .then((orders) => getInvoice(orders[0].id))
  .then((invoice) => console.log(invoice.total))
  .catch(handleError);

错误如何通过链条传播

被拒绝的 Promise 会跳过每个 then,直到到达 catch。将其扔进处理程序内也算作拒绝。正常返回的 catch 使链回到正轨:

Promise.resolve("start")
  .then(() => {
    throw new Error("step 2 failed");
  })
  .then(() => console.log("skipped"))
  .catch((error) => {
    console.log("caught:", error.message);
    return "recovered";
  })
  .then((value) => console.log("continues:", value));

// caught: step 2 failed
// continues: recovered

将此视为 try 和 catch 的异步版本。末尾的一个 catch 通常覆盖整个链。如果您只想记录错误但仍然失败,请从 catch 内部再次抛出它。

提示: fetch 仅因网络故障而拒绝。 404 或 500 响应仍然满足,因此请自行检查 response.ok。我们的 HTTP 状态代码备忘单 解释了每个代码的含义。

Promise.all、allSettled、race 和 any

这些静态方法同时协调多个 Promise。它们都接受一个数组并返回一个承诺。

方法 满足时 拒绝时 用它来
Promise.all 全部满足 任何人都拒绝 所有结果均为必填项
Promise.allSettled 一切安定 从来没有 你想要每一个结果
Promise.race 第一个结算满足 第一个解决拒绝问题 超时
Promise.any 第一个满足 全部拒绝 第一个成功的源码

为了测试它们,我们使用了两个小助手,一个在延迟后成功,另一个在延迟后失败:

const wait = (ms, value) =>
  new Promise((resolve) => setTimeout(() => resolve(value), ms));
const fail = (ms, message) =>
  new Promise((_, reject) => setTimeout(() => reject(new Error(message)), ms));

Promise.all

await Promise.all([wait(300, "a"), wait(100, "b"), wait(200, "c")]);

// [ 'a', 'b', 'c' ]   took about 300 ms

有两个细节需要注意。结果保持输入顺序,即使 b 首先完成。总时间约为 300 毫秒,这是最慢任务的时间,而不是 600 毫秒的总和,因为这三个任务已经同时运行。如果其中任何一个拒绝,整个事情就会立即拒绝:

await Promise.all([wait(100, "a"), fail(50, "b failed")]);

// rejected: b failed

Promise.allSettled

这会等待一切并报告每个结果,因此一次失败不会丢失其余的结果。它适合每个小部件独立加载的仪表板:

const results = await Promise.allSettled([wait(50, "a"), fail(50, "b failed")]);
results.map((r) => r.status);

// [ 'fulfilled', 'rejected' ]

Promise.race 和 Promise.any

await Promise.race([wait(300, "slow"), wait(100, "fast")]);   // fast
await Promise.any([fail(50, "x"), wait(100, "ok")]);          // ok

不同之处在于,race 会先解决,即使它失败了,而 any 只要成功,就会忽略失败。 race 的一个经典用法是为缓慢的请求添加超时,尽管较慢的任务会继续在后台运行,除非您取消它。

Promise 和 async/await

async 和 await 是使用相同 Promise 的更易读的方式。 async 函数总是返回一个 Promise,而 await 会暂停该函数,直到 Promise 解决:

async function showInvoice(id) {
  try {
    const user = await getUser(id);
    const orders = await getOrders(user.id);
    console.log(orders.length);
  } catch (error) {
    console.log("Failed:", error.message);
  }
}

拒绝会变成抛出错误,因此普通的 try 和 catch 可以工作。您仍然需要 Promise.all 来进行并行工作,因为等待一个又一个调用会按顺序运行它们。我们的 JavaScript async/await 指南对此进行了更深入的探讨。

Promise 和事件循环

即使承诺已经解决,承诺处理程序也不会立即被调用。它们在当前代码完成之后、任何计时器之前运行。这是证明:

setTimeout(() => console.log("timeout"), 0);
Promise.resolve().then(() => console.log("microtask"));
console.log("sync");

// sync
// microtask
// timeout

同步代码首先运行。接下来是微任务队列,它保存 Promise 处理程序。只有这样,事件循环才会开始执行下一个任务,例如计时器。这是一个流行的面试问题,它解释了为什么 setTimeout 为零并不是真正立竿见影的。

Promise 与回调

关注 回调 承诺
测序 嵌套,每一步都更深 扁平链条
错误 每个级别都经过检查 一根catch即可覆盖链条
并行工作 手动计数器 Promise.all
误拨两次电话? 可能 不能两次结算

常见错误

  • 忘记返回内部then。如果没有return,下一步将收到undefined,并且不会等待您内心的承诺。
  • 根本没有catch。 这会导致未处理的拒绝,并且在 Node.js 中默认停止该进程。
  • 嵌套而不是链接。将then放入then中会重新创建回调地狱。返回承诺并继续链条。
  • 在new Promise中包装一个promise。如果一个函数已经返回一个promise,那么就使用它。
  • 将forEach与异步回调结合使用。它不会等待。将 for...of 或 Promise.all 与 map 结合使用,如我们的 JavaScript 数组方法指南 中所示。
  • 认为 Promise 可以取消。 它本身不能取消。对于请求,请使用 AbortController。

最后的话

Promise 归结为一些规则:promise 一次解决,then 返回一个新的 Promise,错误沿着链向下传播,直到有东西捕获它们。一旦这些规则感觉很自然,async 和 await 就不再那么神奇了。为了练习,使用 Promise 重写一个基于回调的函数,然后使用 Promise.all 获取两个端点并正确处理失败。

常见问题

JavaScript 中的 Promise 是什么?

Promise 是一个代表异步工作的最终结果的对象。它开始时处于待处理状态,然后要么满足一个值,要么拒绝一个原因。

then 和 catch 有什么区别?

then 在承诺履行时运行,catch 在拒绝时运行。 catch 是有效的,然后只使用拒绝处理程序,并且它还可以捕获在前面的步骤中抛出的错误。

Promise.all 和 Promise.allSettled 之间有什么区别?

一旦一个 Promise 被拒绝,Promise.all 就会拒绝。 Promise.allSettled 始终等待所有这些并报告每个的状态和值或原因。

我应该使用 Promise 还是 async/await?

他们一起工作,因为等待是建立在承诺之上的。大多数代码使用 async/await 读起来效果最好,而 Promise.all 和类似的方法对于并行工作仍然有用。

Promise 是否并行运行?

JavaScript 在一个线程上一次运行一段代码,但像网络请求一样的工作是在后台发生的。其中几个可以同时进行。

继续学习

与Stack Quora合作

需要测试、WordPress 或 SEO 方面的帮助?

我们帮助团队交付更好的软件并发展他们的网站。告诉我们您正在做什么,我们将建议明确的下一步。

继续阅读

更多指南 测试、人工智能和现代开发

喜欢这篇文章吗?选择一个主题并直接跳至我们最新的教程。

人工智能

人工智能幻觉解释:语言模型为何会产生幻觉以及如何减少它

人工智能幻觉解释:它们是什么,为什么语言模型产生它们,常见类型,如何通过基础和提示来减少它们,以及我们进行的简单检查。

阅读文章 →
DevOps

环境变量解释:.env 文件、秘密和配置正确完成

使用我们运行的 Python 示例解释环境变量:读取和设置它们、为什么每个值都是字符串、.env 文件、进程继承、秘密和验证。

阅读文章 →
软件开发

API 速率限制解释:算法、标头和处理 429 错误

API 速率限制通过我们运行的模拟进行了解释:令牌桶、固定窗口、滑动窗口、429 响应、速率限制标头、带回退的重试和测试限制。

阅读文章 →
测试自动化

Python 中的模拟:unittest.mock、补丁以及导致测试谎言的错误

使用unittest.mock 在Python 中进行模拟,经过测试:模拟和return_value、side_effect、修补使用名称的位置、spec 和autospec、模拟日期以及常见错误。

阅读文章 →
软件测试

代码覆盖率解释:行覆盖率与分支覆盖率以及 100% 的真正含义

用真实的coverage.py运行来解释代码覆盖率:行、分支和条件覆盖率,阅读报告,为什么100%覆盖率仍然会错过错误,以及合理的目标。

阅读文章 →
数据库与 SQL

SQL 中的 DELETE、TRUNCATE 和 DROP:差异、回滚以及何时使用它们

DELETE、TRUNCATE 和 DROP 通过经过测试的 SQL 进行了解释:各自删除什么、回滚、触发器、身份重置、速度,以及 PostgreSQL、MySQL、SQL Server 和 SQLite 有何不同。

阅读文章 →
数据库与 SQL

SQL 视图解释:创建、使用和避免陷阱(附示例)

SQL 视图通过我们运行的查询进行解释:创建视图、实时结果、隐藏列、性能、可更新和物化视图、破坏的依赖关系和最佳实践。

阅读文章 →
软件开发

TypeScript 接口与类型:真正的差异,使用编译器进行测试

TypeScript 接口与类型,使用真实编译器进行检查:两者都可以做什么,只有一个可以做什么,声明合并,扩展与交集以及要遵循的简单规则。

阅读文章 →
软件开发

Python *args 和 **kwargs 解释:灵活的函数和清晰的示例

Python *args 和 **kwargs 用我们运行的代码进行了解释:打包和解包、参数顺序、仅关键字和仅位置参数、装饰器和常见错误。

阅读文章 →
软件开发

Python Lambda 函数:语法、实际用途以及何时不使用它们

Python lambda 函数用我们运行的代码进行了解释:语法、使用键排序、映射和过滤器、闭包、后期绑定陷阱,以及何时普通 def 是更好的选择。

阅读文章 →
人工智能

什么是人工智能代理?它们是如何工作的,只需运行一个简单的代理循环

什么是人工智能代理?关于代理循环、工具、内存和护栏的简单指南,以及我们运行的一个小型 Python 循环、真正的风险以及何时简单的聊天机器人是更好的选择。

阅读文章 →
DevOps

监控与可观察性:真正的区别,包括日志、指标和跟踪

监控与可观察性解释:已知和未知的故障、日志、指标和跟踪、检查事件演练、黄金信号、OpenTelemetry 和有用的警报。

阅读文章 →
DevOps

Docker Volumes 与 Bind Mounts:以正确的方式保留容器数据

Docker 卷与绑定挂载解释:为什么容器数据消失、命名卷、绑定挂载、tmpfs、Compose 示例、备份、权限以及何时使用它们。

阅读文章 →
测试自动化

TDD 与 BDD:主要区别、以两种方式构建的一个功能以及何时使用每种方式

TDD 与 BDD 的解释是通过一种双向构建的折扣功能:使用我们运行的代码进行红绿重构、Given-When-Then 场景、工具、权衡以及何时使用每种方案。

阅读文章 →
软件测试

用户验收测试 (UAT):流程、清单和真实示例

用户验收测试解释:谁运行它、它与 QA 有何不同、七步流程、UAT 测试用例模板、清单、签核标准和常见错误。

阅读文章 →
数据库与 SQL

SQL NULL 值解释:IS NULL、COALESCE、NULLIF 和要避免的陷阱

通过我们运行的查询解释了 SQL NULL 值:三值逻辑、IS NULL、COALESCE、NULLIF,聚合、连接和排序如何处理 NULL,以及它悄悄引起的错误。

阅读文章 →
数据库与 SQL

SQL CASE WHEN 语句:语法、实例和常见错误

SQL CASE WHEN 解释了我们运行的查询:简单和搜索表单、条件顺序、SUM 透视、COUNT 技巧、自定义排序、更新和要避免的 NULL 陷阱。

阅读文章 →
软件开发

什么是智威汤逊?通过工作示例解释 JSON Web 令牌

什么是智威汤逊? JSON Web Tokens 解释:这三个部分,在我们运行的代码中构建和篡改的令牌,JWT 与会话,存储,刷新令牌和安全错误。

阅读文章 →
软件开发

JavaScript 事件循环解释:调用堆栈、微任务和计时器以及示例

JavaScript 事件循环用我们运行的代码进行了解释:调用堆栈、任务和微任务队列、为什么 setTimeout 0 不是立即执行、阻塞、饥饿和面试测验。

阅读文章 →
软件开发

Python 生成器和 Yield 解释:通过真实示例节省内存

Python 生成器和yield 用我们运行的代码进行了解释:它们如何暂停和恢复、测量内存节省、管道、无限序列、yield from、发送和常见陷阱。

阅读文章 →
人工智能

什么是嵌入?向量和余弦相似度简单解释

什么是嵌入?关于向量、余弦相似度和语义搜索的简单语言指南,其中包含我们运行的一个小示例、常见用途、选择模型和已知限制。

阅读文章 →
DevOps

蓝绿部署与金丝雀部署:差异、权衡以及如何选择

蓝绿部署与金丝雀部署解释:每种部署的工作原理、成本、风险和回滚、我们测试的稳定用户分桶、数据库迁移、滚动更新以及如何选择。

阅读文章 →
测试自动化

不稳定的测试:造成这些问题的原因以及如何彻底解决它们

我们运行的演示解释了不稳定的测试:主要原因、如何确认和诊断、正确的时间修复、共享状态和日期以及隔离过程。

阅读文章 →
软件测试

负载测试与压力测试:差异、指标和 k6 示例

负载测试与压力测试的解释:各自证明什么、峰值和浸泡测试、重要的指标、为什么平均值会误导、k6 个示例以及如何计划测试。

阅读文章 →
软件测试

测试用例与测试场景:主要区别、示例以及何时使用它们

测试用例与测试场景的解释:清晰的定义、并排的表格、从场景到案例的密码重置功能、模板以及何时使用每个功能。

阅读文章 →
数据库与 SQL

SQL 查询优化:读取执行计划并修复慢速查询(带计时)

对一百万行进行 SQL 查询优化测试:读取执行计划、添加正确的索引、修复不可控制的过滤器、使用复合索引和键集分页。

阅读文章 →
数据库与 SQL

SQL UNION 与 UNION ALL:差异、示例和性能测试

SQL UNION 与 UNION ALL 通过我们运行的查询进行了解释:重复、都遵循的规则、真实的数据错误、测量的性能、INTERSECT 和 EXCEPT 以及何时使用它们。

阅读文章 →
软件开发

JavaScript Fetch API 教程:GET、POST、错误处理和超时

针对本地服务器测试的 JavaScript Fetch API 教程:GET 和 POST、为什么不抛出 404、可重用的帮助程序、查询字符串、超时、CORS 和常见错误。

阅读文章 →
软件开发

Python 文件处理:读取、写入、CSV、JSON 和 pathlib 以及示例

使用我们运行的代码解释了 Python 文件处理:打开模式、使用、读取大文件、CSV、JSON、pathlib、二进制文件、安全写入、错误处理和常见错误。

阅读文章 →
软件开发

Python 类和对象:带有实际示例的 OOP 教程

用我们运行的代码解释 Python 类和对象:__init__、方法、类与实例属性、继承、属性、dunder 方法、数据类和常见错误。

阅读文章 →
人工智能

什么是大语言模型?法学硕士如何运作,在没有炒作的情况下进行解释

什么是大语言模型?法学硕士用简单的语言解释:标记、嵌入、注意力、训练、温度和上下文窗口、一个微小的下一个单词演示和真正的限制。

阅读文章 →
DevOps

适合初学者的 Linux 命令:带有示例的基本终端指南

适合初学者的 Linux 命令,按任务分组:导航、文件、查看日志、grep、管道、权限、进程和档案,并附有实用的日志分析示例。

阅读文章 →
测试自动化

Selenium 与 Playwright:主要区别、代码比较以及选择

Selenium 与 Playwright 比较:架构、等待、浏览器和语言支持、调试、两种工具中的相同测试、迁移建议以及如何选择。

阅读文章 →
软件测试

等价划分:通过实例解释测试设计技术

等效分区解释:如何找到有效和无效分区,年龄、密码和优惠券的工作示例,我们运行的测试,以及为什么将其与边界配对。

阅读文章 →
软件测试

软件测试中的严重性与优先级:差异、矩阵和实例

软件测试中的严重性与优先级解释:清晰的定义、带有示例的四向矩阵、级别尺度、谁设置每个级别以及如何运行错误分类。

阅读文章 →
数据库与 SQL

SQL 子查询教程:类型、相关子查询和 EXISTS 以及示例

SQL 子查询教程,其中包含我们运行的查询:标量、IN、FROM 和 SELECT 子查询、相关子查询、EXISTS、NOT IN NULL 陷阱以及子查询与 JOIN 与 CTE。

阅读文章 →
数据库与 SQL

SQL 事务和 ACID 解释:COMMIT、ROLLBACK 和隔离级别

SQL 事务和 ACID 通过我们运行的银行转账进行了解释:COMMIT、ROLLBACK、保存点、锁、隔离级别、脏读、死锁和最佳实践。

阅读文章 →
软件开发

Git 命令备忘单:您实际使用的命令,以及示例

Git 命令备忘单按任务分组:设置、暂存、分支、远程、存储、历史记录和撤消错误,以及来自真实演示存储库的输出。

阅读文章 →
软件开发

JavaScript Promise 解释:then、catch、Promise.all 和常见错误

JavaScript 承诺用我们运行的代码进行了解释:状态、then 和 catch 链、错误处理、Promise.all、allSettled、race 和 any、事件循环和常见错误。

阅读文章 →
软件开发

Python 字典教程:方法、示例和常见模式

Python 字典教程,包含我们运行的代码:创建、读取和更新字典、关键方法、合并、理解、计数和分组模式以及常见错误。

阅读文章 →
人工智能

什么是 RAG?通过工作示例解释检索增强生成

什么是RAG?检索增强生成逐步解释:索引、分块、嵌入、检索、一个小型的 Python 工作示例、评估和常见故障。

阅读文章 →
DevOps

Terraform 初学者教程:从第一个文件到模块的基础设施即代码

适合初学者的 Terraform 教程:从没有云帐户开始,学习初始化、计划和应用,然后是变量、输出、状态、模块和安全的团队工作流程。

阅读文章 →
DevOps

Docker 与 Kubernetes:主要区别、它们如何协同工作以及您需要哪些

Docker 与 Kubernetes 的解释:各自的作用、同一个应用程序同时运行、Docker Compose 和 Swarm 的比较、dockershim 的变化以及如何决定您需要什么。

阅读文章 →
测试自动化

Pytest 初学者教程:夹具、参数化和真实示例

面向初学者的 pytest 教程,其中包含我们在 pytest 9.2 上运行的测试:断言、固定装置、参数化、conftest、标记、tmp_path、monkeypatch、有用的标志和 CI。

阅读文章 →
软件测试

软件测试生命周期 (STLC):6 个阶段、进入和退出标准、示例

软件测试生命周期解释:六个 STLC 阶段,包括进入和退出标准、可交付成果、有效的登录功能示例、STLC 与 SDLC 以及敏捷中的 STLC。

阅读文章 →
软件开发

Git merge 与 rebase 的区别:示例及如何选择

用真实的 log 输出、对比表、rebase 黄金法则和安全的团队工作流,讲清 Git merge 与 rebase 的区别。

阅读文章 →
软件开发

Python 列表推导式:语法、示例与何时避免使用

Python 列表推导式详解:语法、过滤、if-else、嵌套循环、实测耗时,以及何时普通 for 循环更合适。

阅读文章 →
数据库与 SQL

SQL JOIN 详解:附示例(INNER、LEFT、RIGHT、FULL)

通过示例表和查询,一步步讲解 SQL JOIN:INNER、LEFT、RIGHT、FULL、CROSS 和 SELF,并介绍常见错误与性能技巧。

阅读文章 →