created_at
updated_at
tags
toc
headings

Promise.withResolvers によるイベントの Promise 化

Intro

Promise.withResolvers() は、 Stage 4 であり ES2024 の候補となった。

すでにブラウザでの実装も進んでいるため、その活用方法を解説する。

イベントの Promise 化

JS では、非同期処理 API は長らくイベントリスナベースで定義され、それを組み合わせるフロー制御で処理されてきた。

しかし、 Promise が定義されて以降は、標準 API も Promise を返すようになり、 async/await によって処理されるのが一般的になってきた。

結果、イベントリスナベースの API を Promise 化するような場面も増えた。

例えば以下のようなものだ。

async function request() {
  return new Promise((resolve, reject) => {
    document.querySelector("button").addEventListener("click", async () => {
      try {
        const res = await fetch("/")
        const body = await res.text()
        resolve(body)
      } catch (err) {
        reject(err)
      }
    })
  })
}

resolve/reject がコールバックの引数で渡されるため、このようにネストしたコードになりがちだ。

resolve/reject

ここで resolve/reject を取り出す、以下のような書き方がなされることがある。

async function request() {
  let resolve, reject
  const promise = new Promise((res, rej) => {
    resolve = res
    reject = rej
  })
  document.querySelector("button").addEventListener("click", async () => {
    try {
      const res = await fetch("/")
      const body = res.text()
      resolve(body)
    } catch (err) {
      reject(err)
    }
  })
  return promise
}

この取り出す部分を共通化すると以下のようになる。

function withResolvers() {
  let resolve, reject
  const promise = new Promise((res, rej) => {
    resolve = res
    reject = rej
  })
  return { resolve, reject, promise }
}

このような Utility がよく使われているため、それを標準化しようという提案がなされた。

Promise.withResolvers()

これを標準化したのが Promise.withResolvers() だ。

これを用いると、以下のように書くことができる。

async function request() {
  const { promise, resolve, reject } = Promise.withResolvers()
  document.querySelector("button").addEventListener("click", async () => {
    try {
      const res = await fetch("/")
      const body = res.text()
      resolve(body)
    } catch (err) {
      reject(err)
    }
  })
  return promise
}

例えば以下のように使えば、 EventTarget を継承した API の Promise 化もできる。

async function readFile(file: File) {
  const { promise, resolve, reject } = Promise.withResolvers()
  const reader = new FileReader()
  reader.onload = resolve
  reader.onerror = reject
  reader.onabort = reject
  reader.readAsText(file)
  return promise
}

既にブラウザへの実装も進んでいる。

なお、 Firefox は内部で PromiseUtils.defer() という名前で実装していたが、現在は Promise.withResolvers() に置き換えられている。

Outro

イベントのハンドリングを Promise でラップして返すパターンは一般的であるため、今後このような処理を書く際に導入を検討できるだろう。

また、同等の Utility を用いてる場合は、置き換えを検討できる。

DEMO

動作するデモを以下に用意した。

Resources