Promise

Baseline 广泛可用 *

此特性已相当成熟,可在许多设备和浏览器版本上使用。自 ⁨2015 年 7 月⁩以来,各浏览器均已提供此特性。

* 此特性的某些部分可能存在不同级别的支持。

Promise 对象代表一个异步操作的最终完成(或失败)及其结果值。

要了解 Promise 的工作原理以及如何使用它们,建议您先阅读使用 Promise

描述

Promise 是一个值的代理,该值在创建 Promise 时不一定已知。它允许你将处理程序与异步操作的最终成功值或失败原因关联起来。这使得异步方法可以像同步方法一样返回值:异步方法不是立即返回最终值,而是返回一个 promise,以便在未来的某个时间点提供该值。

一个 Promise 处于以下状态之一:

  • 待定 (pending):初始状态,既未完成也未拒绝。
  • 已完成 (fulfilled):意味着操作成功完成。
  • 已拒绝 (rejected):意味着操作失败。

一个待定 Promise 的最终状态 可以是带有值的已完成,也可以是带有原因(错误)的已拒绝。当出现这两种情况之一时,通过 Promise 的 then 方法排队的关联处理程序就会被调用。如果 Promise 在关联的处理程序被添加时已经已完成或已拒绝,则处理程序将被调用,因此异步操作完成与其处理程序被添加之间没有竞争条件。

如果 Promise 已完成或已拒绝,但不是待定状态,则称其为已敲定 (settled)

Flowchart showing how the Promise state transitions between pending, fulfilled, and rejected via then/catch handlers. A pending promise can become either fulfilled or rejected. If fulfilled, the "on fulfillment" handler, or first parameter of the then() method, is executed and carries out further asynchronous actions. If rejected, the error handler, either passed as the second parameter of the then() method or as the sole parameter of the catch() method, gets executed.

你还会听到“已解决 (resolved)”一词用于 Promise——这意味着 Promise 已经敲定,或者“锁定”为匹配另一个 Promise 的最终状态,并且进一步解决或拒绝它没有效果。原始 Promise 提案中的 States and fates 文档包含有关 Promise 术语的更多细节。口语中,“已解决”的 Promise 通常等同于“已完成”的 Promise,但正如“States and fates”中所示,已解决的 Promise 也可以是待定或已拒绝的。例如:

js
new Promise((resolveOuter) => {
  resolveOuter(
    new Promise((resolveInner) => {
      setTimeout(resolveInner, 1000);
    }),
  );
});

这个 Promise 在创建时就已经已解决(因为 resolveOuter 是同步调用的),但它是用另一个 Promise 解决的,因此直到 1 秒后内部 Promise 完成时才会被完成。实际上,“解决”通常在幕后完成,不可观察,只有其完成或拒绝才可观察。

注意:其他一些语言也有延迟计算和推迟计算的机制,它们也称之为“Promise”,例如 Scheme。JavaScript 中的 Promise 表示已经发生的进程,可以与回调函数链式调用。如果你想延迟评估一个表达式,可以考虑使用一个没有参数的函数,例如 f = () => expression 来创建延迟评估的表达式,然后使用 f() 来立即评估表达式。

Promise 本身没有用于取消的一等协议,但你可能能够直接取消底层的异步操作,通常使用 AbortController

链式 Promise

Promise 方法 then()catch()finally() 用于将后续操作与已敲定的 Promise 关联起来。then() 方法最多接受两个参数;第一个参数是 Promise 完成情况的回调函数,第二个参数是拒绝情况的回调函数。catch()finally() 方法在内部调用 then(),使错误处理不那么冗长。例如,catch() 实际上只是一个不传递完成处理程序的 then()。由于这些方法返回 Promise,它们可以被链式调用。例如:

js
const myPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("foo");
  }, 300);
});

myPromise
  .then(handleFulfilledA, handleRejectedA)
  .then(handleFulfilledB, handleRejectedB)
  .then(handleFulfilledC, handleRejectedC);

我们将使用以下术语:初始 Promise 是调用 then 的 Promise;新 Promisethen 返回的 Promise。传递给 then 的两个回调分别称为完成处理程序拒绝处理程序

初始 Promise 的敲定状态决定了执行哪个处理程序。

  • 如果初始 Promise 已完成,则使用完成值调用完成处理程序。
  • 如果初始 Promise 已拒绝,则使用拒绝原因调用拒绝处理程序。

处理程序的完成决定了新 Promise 的敲定状态。

  • 如果处理程序返回一个thenable值,则新 Promise 以返回值的相同状态敲定。
  • 如果处理程序返回一个非 thenable 值,则新 Promise 以返回值完成。
  • 如果处理程序抛出错误,则新 Promise 以抛出的错误拒绝。
  • 如果初始 Promise 没有关联的相应处理程序,则新 Promise 将以与初始 Promise 相同的状态敲定——也就是说,如果没有拒绝处理程序,被拒绝的 Promise 将以相同的原因保持拒绝状态。

例如,在上面的代码中,如果 myPromise 拒绝,则会调用 handleRejectedA,如果 handleRejectedA 正常完成(没有抛出或返回被拒绝的 Promise),则第一个 then 返回的 Promise 将被完成而不是保持拒绝。因此,如果必须立即处理错误,但我们希望在链中保持错误状态,则必须在拒绝处理程序中抛出某种类型的错误。另一方面,在没有立即需要的情况下,我们可以将错误处理推迟到最终的 catch() 处理程序。

js
myPromise
  .then(handleFulfilledA)
  .then(handleFulfilledB)
  .then(handleFulfilledC)
  .catch(handleRejectedAny);

对于回调函数使用箭头函数,Promise 链的实现可能如下所示:

js
myPromise
  .then((value) => `${value} and bar`)
  .then((value) => `${value} and bar again`)
  .then((value) => `${value} and again`)
  .then((value) => `${value} and again`)
  .then((value) => {
    console.log(value);
  })
  .catch((err) => {
    console.error(err);
  });

注意:为了更快的执行,所有同步操作最好在一个处理程序内完成,否则将需要几个时间片(tick)才能按顺序执行所有处理程序。

JavaScript 维护一个任务队列。每次,JavaScript 从队列中取出一个任务并执行它直到完成。任务由 Promise() 构造函数的执行器、传递给 then 的处理程序或任何返回 Promise 的平台 API 定义。链中的 Promise 表示这些任务之间的依赖关系。当一个 Promise 敲定后,与其关联的相应处理程序将被添加到任务队列的末尾。

一个 Promise 可以参与多个链。对于以下代码,promiseA 的完成将导致 handleFulfilled1handleFulfilled2 都被添加到任务队列中。因为 handleFulfilled1 首先注册,它将首先被调用。

js
const promiseA = new Promise(myExecutorFunc);
const promiseB = promiseA.then(handleFulfilled1, handleRejected1);
const promiseC = promiseA.then(handleFulfilled2, handleRejected2);

一个动作可以分配给一个已经敲定的 Promise。在这种情况下,该动作会立即添加到任务队列的末尾,并在所有现有任务完成后执行。因此,对一个已经“敲定”的 Promise 的动作只会在当前同步代码完成后,并且至少经过一个事件循环(loop-tick)后才发生。这保证了 Promise 动作是异步的。

js
const promiseA = new Promise((resolve, reject) => {
  resolve(777);
});
// At this point, "promiseA" is already settled.
promiseA.then((val) => console.log("asynchronous logging has val:", val));
console.log("immediate logging");

// produces output in this order:
// immediate logging
// asynchronous logging has val: 777

Thenable

在 Promise 成为语言的一部分之前,JavaScript 生态系统已经有了多种 Promise 实现。尽管它们在内部表示方式不同,但所有类 Promise 对象至少都实现了 Thenable 接口。一个 thenable 实现 .then() 方法,该方法接受两个回调:一个用于 Promise 完成时,一个用于 Promise 拒绝时。Promise 也是 thenable。

为了与现有的 Promise 实现互操作,该语言允许使用 thenable 代替 Promise。例如,Promise.resolve 不仅会解决 Promise,还会追踪 thenable。

js
const thenable = {
  then(onFulfilled, onRejected) {
    onFulfilled({
      // The thenable is fulfilled with another thenable
      then(onFulfilled, onRejected) {
        onFulfilled(42);
      },
    });
  },
};

Promise.resolve(thenable); // A promise fulfilled with 42

Promise 并发

Promise 类提供了四个静态方法来方便异步任务的并发

Promise.all()

所有 Promise 都完成时完成;当任何一个 Promise 拒绝时拒绝。

Promise.allSettled()

所有 Promise 都敲定时完成。

Promise.any()

任何一个 Promise 完成时完成;当所有 Promise 都拒绝时拒绝。

Promise.race()

任何一个 Promise 敲定时敲定。换句话说,当任何一个 Promise 完成时完成;当任何一个 Promise 拒绝时拒绝。

所有这些方法都接受一个 Promise 的可迭代对象(确切地说是thenable)并返回一个新的 Promise。它们都支持子类化,这意味着它们可以在 Promise 的子类上调用,结果将是子类类型的 Promise。为此,子类的构造函数必须实现与 Promise() 构造函数相同的签名——接受一个可以以 resolvereject 回调作为参数调用的单一 executor 函数。子类还必须有一个 resolve 静态方法,可以像 Promise.resolve() 一样调用,以将值解析为 Promise。

请注意,JavaScript 本质上是单线程的,因此在给定瞬间,只有一个任务会执行,尽管控制可以在不同的 Promise 之间切换,使得 Promise 的执行看起来是并发的。JavaScript 中的并行执行只能通过工作线程来实现。

构造函数

Promise()

创建一个新的 Promise 对象。该构造函数主要用于包装尚不支持 Promise 的函数。

静态属性

Promise[Symbol.species]

返回用于从 Promise 方法构造返回值的构造函数。

静态方法

Promise.all()

接受一个 Promise 的可迭代对象作为输入,并返回一个单一的 Promise。当所有输入的 Promise 都完成时(包括传递空可迭代对象时),这个返回的 Promise 会完成,其值为一个包含所有完成值的数组。当任何一个输入的 Promise 拒绝时,它会以第一个拒绝原因拒绝。

Promise.allSettled()

接受一个 Promise 的可迭代对象作为输入,并返回一个单一的 Promise。当所有输入的 Promise 都敲定(包括传递空可迭代对象时),这个返回的 Promise 会完成,其值为一个描述每个 Promise 结果的对象的数组。

Promise.any()

接受一个 Promise 的可迭代对象作为输入,并返回一个单一的 Promise。当任何一个输入的 Promise 完成时,这个返回的 Promise 会以第一个完成值完成。当所有输入的 Promise 都拒绝时(包括传递空可迭代对象时),它会以一个包含拒绝原因数组的 AggregateError 拒绝。

Promise.race()

接受一个 Promise 的可迭代对象作为输入,并返回一个单一的 Promise。这个返回的 Promise 会以第一个敲定的 Promise 的最终状态敲定。

Promise.reject()

返回一个新的 Promise 对象,该对象以给定的原因拒绝。

Promise.resolve()

返回一个用给定值解决的 Promise 对象。如果该值是一个 thenable(即,有一个 then 方法),则返回的 Promise 将“跟随”该 thenable,采用其最终状态;否则,返回的 Promise 将用该值完成。

Promise.try()

接受一个任何类型的回调(同步或异步返回或抛出),并将其结果包装在 Promise 中。

Promise.withResolvers()

返回一个对象,其中包含一个新的 Promise 对象以及两个用于解决或拒绝它的函数,对应于传递给 Promise() 构造函数的执行器的两个参数。

实例属性

这些属性在 Promise.prototype 上定义,并由所有 Promise 实例共享。

Promise.prototype.constructor

创建实例对象的构造函数。对于 Promise 实例,初始值是 Promise 构造函数。

Promise.prototype[Symbol.toStringTag]

[Symbol.toStringTag] 属性的初始值是字符串 "Promise"。此属性用于 Object.prototype.toString()

实例方法

Promise.prototype.catch()

将拒绝处理程序回调添加到 Promise,并返回一个新的 Promise,如果调用回调,则解析为回调的返回值,如果 Promise 完成,则解析为原始完成值。

Promise.prototype.finally()

将一个处理程序附加到 Promise,并返回一个当原始 Promise 解决时解决的新 Promise。无论 Promise 完成还是拒绝,都会调用该处理程序。

Promise.prototype.then()

将完成和拒绝处理程序附加到 Promise,并返回一个解析为被调用处理程序的返回值的新 Promise,如果 Promise 未被处理(即,如果相关的处理程序 onFulfilledonRejected 不是函数),则解析为原始敲定值。

示例

基本示例

在此示例中,我们使用 setTimeout(...) 来模拟异步代码。实际上,你可能会使用类似 XHR 或 HTML API 的东西。

js
const myFirstPromise = new Promise((resolve, reject) => {
  // We call resolve(...) when what we were doing asynchronously
  // was successful, and reject(...) when it failed.
  setTimeout(() => {
    resolve("Success!"); // Yay! Everything went well!
  }, 250);
});

myFirstPromise.then((successMessage) => {
  // successMessage is whatever we passed in the resolve(...) function above.
  // It doesn't have to be a string, but if it is only a succeed message, it probably will be.
  console.log(`Yay! ${successMessage}`);
});

不同情况的示例

此示例展示了使用 Promise 功能的各种技术和可能发生的各种情况。要理解这一点,请从代码块底部开始,检查 Promise 链。在提供初始 Promise 后,可以跟随一系列 Promise。该链由 .then() 调用组成,通常(但不一定)在末尾有一个单一的 .catch(),可选地后跟 .finally()。在此示例中,Promise 链由自定义编写的 new Promise() 构造启动;但在实际实践中,Promise 链通常从返回 Promise 的 API 函数(由其他人编写)开始。

示例函数 tetheredGetNumber() 展示了 Promise 生成器在设置异步调用时、在回调中或两者中都会使用 reject()。函数 promiseGetWord() 说明了 API 函数如何以自包含的方式生成并返回 Promise。

请注意,函数 troubleWithGetNumber()throw 结尾。这是强制性的,因为 Promise 链会遍历所有 .then() Promise,即使在发生错误之后,如果没有 throw,错误看起来就会“被修复”。这很麻烦,因此,通常在整个 .then() Promise 链中省略 onRejected,而只在最终的 catch() 中有一个单一的 onRejected

此代码可以在 NodeJS 下运行。通过查看实际发生的错误可以增强理解。要强制更多错误发生,请更改 threshold 值。

js
// To experiment with error handling, "threshold" values cause errors randomly
const THRESHOLD_A = 8; // can use zero 0 to guarantee error

function tetheredGetNumber(resolve, reject) {
  setTimeout(() => {
    const randomInt = Date.now();
    const value = randomInt % 10;
    if (value < THRESHOLD_A) {
      resolve(value);
    } else {
      reject(new RangeError(`Too large: ${value}`));
    }
  }, 500);
}

function determineParity(value) {
  const isOdd = value % 2 === 1;
  return { value, isOdd };
}

function troubleWithGetNumber(reason) {
  const err = new Error("Trouble getting number", { cause: reason });
  console.error(err);
  throw err;
}

function promiseGetWord(parityInfo) {
  return new Promise((resolve, reject) => {
    const { value, isOdd } = parityInfo;
    if (value >= THRESHOLD_A - 1) {
      reject(new RangeError(`Still too large: ${value}`));
    } else {
      parityInfo.wordEvenOdd = isOdd ? "odd" : "even";
      resolve(parityInfo);
    }
  });
}

new Promise(tetheredGetNumber)
  .then(determineParity, troubleWithGetNumber)
  .then(promiseGetWord)
  .then((info) => {
    console.log(`Got: ${info.value}, ${info.wordEvenOdd}`);
    return info;
  })
  .catch((reason) => {
    if (reason.cause) {
      console.error("Had previously handled error");
    } else {
      console.error(`Trouble with promiseGetWord(): ${reason}`);
    }
  })
  .finally((info) => console.log("All done"));

高级示例

这个小例子展示了 Promise 的机制。每次点击 <button> 时都会调用 testPromise() 方法。它创建一个 Promise,该 Promise 将使用 setTimeout() 在随机的 1-3 秒内完成为 Promise 计数(从 1 开始的数字)。Promise() 构造函数用于创建 Promise。

Promise 的完成通过使用 p1.then() 设置的完成回调进行记录。一些日志显示了方法的同步部分如何与 Promise 的异步完成解耦。

通过在短时间内多次点击按钮,你甚至会看到不同的 Promise 一个接一个地完成。

HTML

html
<button id="make-promise">Make a promise!</button>
<div id="log"></div>

JavaScript

js
"use strict";

let promiseCount = 0;

function testPromise() {
  const thisPromiseCount = ++promiseCount;
  const log = document.getElementById("log");
  // begin
  log.insertAdjacentHTML("beforeend", `${thisPromiseCount}) Started<br>`);
  // We make a new promise: we promise a numeric count of this promise,
  // starting from 1 (after waiting 3s)
  const p1 = new Promise((resolve, reject) => {
    // The executor function is called with the ability
    // to resolve or reject the promise
    log.insertAdjacentHTML(
      "beforeend",
      `${thisPromiseCount}) Promise constructor<br>`,
    );
    // This is only an example to create asynchronism
    setTimeout(
      () => {
        // We fulfill the promise
        resolve(thisPromiseCount);
      },
      Math.random() * 2000 + 1000,
    );
  });

  // We define what to do when the promise is resolved with the then() call,
  // and what to do when the promise is rejected with the catch() call
  p1.then((val) => {
    // Log the fulfillment value
    log.insertAdjacentHTML("beforeend", `${val}) Promise fulfilled<br>`);
  }).catch((reason) => {
    // Log the rejection reason
    console.log(`Handle rejected promise (${reason}) here.`);
  });
  // end
  log.insertAdjacentHTML("beforeend", `${thisPromiseCount}) Promise made<br>`);
}

const btn = document.getElementById("make-promise");
btn.addEventListener("click", testPromise);

结果

使用 XHR 加载图像

下面展示了另一个使用 PromiseXMLHttpRequest 加载图像的示例。每个步骤都已注释,让你可以密切关注 Promise 和 XHR 架构。

js
function imgLoad(url) {
  // Create new promise with the Promise() constructor;
  // This has as its argument a function with two parameters, resolve and reject
  return new Promise((resolve, reject) => {
    // XHR to load an image
    const request = new XMLHttpRequest();
    request.open("GET", url);
    request.responseType = "blob";
    // When the request loads, check whether it was successful
    request.onload = () => {
      if (request.status === 200) {
        // If successful, resolve the promise by passing back the request response
        resolve(request.response);
      } else {
        // If it fails, reject the promise with an error message
        reject(
          Error(
            `Image didn't load successfully; error code: + ${request.statusText}`,
          ),
        );
      }
    };
    // Handle network errors
    request.onerror = () => reject(new Error("There was a network error."));
    // Send the request
    request.send();
  });
}

// Get a reference to the body element, and create a new image object
const body = document.querySelector("body");
const myImage = new Image();
const imgUrl =
  "https://mdn.github.io/shared-assets/images/examples/round-balloon.png";

// Call the function with the URL we want to load, then chain the
// promise then() method with two callbacks
imgLoad(imgUrl).then(
  (response) => {
    // The first runs when the promise resolves, with the request.response
    // specified within the resolve() method.
    const imageURL = URL.createObjectURL(response);
    myImage.src = imageURL;
    body.appendChild(myImage);
  },
  (error) => {
    // The second runs when the promise
    // is rejected, and logs the Error specified with the reject() method.
    console.log(error);
  },
);

当前设置对象跟踪

设置对象是一个环境,它在 JavaScript 代码运行时提供额外信息。这包括 Realm 和模块映射,以及 HTML 特定信息,例如来源。跟踪当前设置对象是为了确保浏览器知道在给定用户代码片段中使用哪个对象。

为了更好地理解这一点,我们可以仔细看看 Realm 可能是一个问题的地方。Realm 大致可以看作是全局对象。Realm 的独特之处在于它们包含运行 JavaScript 代码所需的所有必要信息。这包括像 ArrayError 这样的对象。每个设置对象都有自己的这些“副本”,并且它们不共享。这可能导致与 Promise 相关的一些意外行为。为了解决这个问题,我们跟踪一个称为当前设置对象的东西。它表示特定于负责某个函数调用的用户代码上下文的信息。

为了进一步说明这一点,我们可以看看文档中嵌入的 <iframe> 如何与其宿主通信。由于所有 Web API 都知道当前设置对象,因此以下内容将在所有浏览器中工作:

html
<!doctype html>
<iframe></iframe>
<!-- we have a realm here -->
<script>
  // we have a realm here as well
  const bound = frames[0].postMessage.bind(frames[0], "some data", "*");
  // bound is a built-in function — there is no user
  // code on the stack, so which realm do we use?
  setTimeout(bound);
  // this still works, because we use the youngest
  // realm (the incumbent) on the stack
</script>

同样的概念也适用于 Promise。如果我们将上面的例子稍作修改,我们会得到以下结果:

html
<!doctype html>
<iframe></iframe>
<!-- we have a realm here -->
<script>
  // we have a realm here as well
  const bound = frames[0].postMessage.bind(frames[0], "some data", "*");
  // bound is a built in function — there is no user
  // code on the stack — which realm do we use?
  Promise.resolve(undefined).then(bound);
  // this still works, because we use the youngest
  // realm (the incumbent) on the stack
</script>

如果我们修改这个例子,让文档中的 <iframe> 监听 post 消息,我们可以观察到当前设置对象的效果:

html
<!-- y.html -->
<!doctype html>
<iframe src="x.html"></iframe>
<script>
  const bound = frames[0].postMessage.bind(frames[0], "some data", "*");
  Promise.resolve(undefined).then(bound);
</script>
html
<!-- x.html -->
<!doctype html>
<script>
  window.addEventListener("message", (event) => {
    document.querySelector("#text").textContent = "hello";
    // this code will only run in browsers that track the incumbent settings object
    console.log(event);
  });
</script>

在上面的例子中,<iframe> 的内部文本只有在跟踪当前设置对象时才会更新。这是因为如果不跟踪当前设置对象,我们可能会使用错误的环境来发送消息。

注意:目前,当前 realm 跟踪在 Firefox 中已完全实现,在 Chrome 和 Safari 中有部分实现。

规范

规范
ECMAScript® 2026 语言规范
# sec-promise-objects

浏览器兼容性

另见