Promise, async, await

個人用のメモなので不正確です。

Promise

console.log('start')
new Promise((resolve, reject) => {
  console.log('Promise start')
  const value = Math.random()
  if(value < 0.5) {
    resolve(value)
  } else {
    reject(value)
  }
  console.log('Promise end')
}).then((value) => {
  console.log('resolveが実行された場合に実行される:', value)
}).catch((error) => {
  console.log('rejectが実行された場合に実行される:', error)
}).finally(() => {
  console.log('必ず最後に実行される')
})
console.log('end')

実行結果

start
Promise start
Promise end
end
resolveが実行された場合に実行される: 0.4217906883305165
必ず最後に実行される

thencatchの実行は非同期で行われる。しかし、Promiseの引数の関数は即座に実行される。

thencatchはresolveやrejectが呼ばれた時ではなく、それらの関数が呼ばれていて、Promiseの引数の関数の実行が終わっている状態なら非同期で実行される。

then

thenはチェインすることができる。

new Promise((r) => r(1))
  .then((v) => v * 2)
  .then((v) => v * 3)
  .then((v) => console.log(v)) // 6が表示される

catch

catchPromiseのrejectが呼ばれた時に実行されるが、例外が発生した場合でも実行される。

new Promise(() => { throw new Error('例外') })
  .then(() => console.log('実行されない'))
  .catch((error) => console.log(error.message)) // 例外 のみが表示される

thenの中で例外が発生した場合も同様にcatchが実行される。catchの中で例外が発生した場合も後続のcatchが実行される。

new Promise((r) => { r(1) })
  .then((v) => { throw new Error('例外') })
  .then(() => console.log('実行されない'))
  .catch((error) => console.log(error.message)) // 例外 のみが表示される

また、catchから戻り値を返して、後続のthenに繋げることもできる。

new Promise((r) => { r(1) })
  .then((v) => { throw new Error('error from then1') })
  .then(() => console.log('実行されない'))
  .catch((error) => {
    console.log(error.message)
    return error.message + ' with catch1'
  })
  .then((v) => { throw new Error(v + ' from then2') })
  .then(() => console.log('実行されない'))
  .catch((error) => {
    console.log(error.message)
    return error.message + ' with catch2'
  })
  .then((v) => console.log('実行される:', v))
// 出力結果
// error from then1
// error from then1 with catch1 from then2
// 実行される: error from then1 with catch1 from then2 with catch2

thenの中でのPromise

例えば、Promiseのresolveの結果を受けて、1つ目のthenでさらにAjaxリクエストを出して、後続のthenに繋げたい場合、単に以下のように書いたのでは機能しない。

import https from "https"

new Promise((resolve, reject) => {
  // JSONを取ってきているだけ
  https.get('https://jsonplaceholder.typicode.com/users/', res => {
    let body = ''
    res.setEncoding('utf8')
    res.on('data', (chunk) => body += chunk)
    res.on('end', () => resolve(body)) // 取得結果をresolveに渡す
  })
}).then((value) => JSON.parse(value))
  .then((value) => {
    console.log(value[0].name)
    return value[0].name
  })
  .then((value) => { // 最初のJSONの結果を使いつつ追加でJSONを取得しようとする
    let all = value
    https.get('https://jsonplaceholder.typicode.com/users/', res => {
      let body = ''
      res.setEncoding('utf8')
      res.on('data', (chunk) => body += chunk)
      res.on('end', () => all = body)
    })
    // 1回目と2回目の結果を合わせたものをreturnしようとしているが
    // 実際にはhttps.getが終わる前にreturnされてしまう
    return all
  }).then((value) => console.log(value))

この問題の解決策は、thennew Promiseして返せば良い。

import https from "https"

new Promise((resolve, reject) => {
  https.get('https://jsonplaceholder.typicode.com/users/', res => {
    let body = ''
    res.setEncoding('utf8')
    res.on('data', (chunk) => body += chunk)
    res.on('end', () => resolve(body))
  })
}).then((value) => JSON.parse(value))
  .then((value) => {
    console.log(value[0].name)
    return value[0].name
  })
  .then((value) => {
    return new Promise((resolve, reject) => {
      https.get('https://jsonplaceholder.typicode.com/users/', res => {
        let body = ''
        res.setEncoding('utf8')
        res.on('data', (chunk) => body += chunk)
        res.on('end', () => resolve(value + JSON.parse(body)[1].name))
      })
    })
    // 最初のリクエストと二回目のリクエストの結果が連結されたものが表示される
  }).then((value) => console.log(value))

thenの第二引数

thenの第二引数に関数を指定するとcatchとして機能する。

new Promise((r) => { r(1) })
  .then((v) => { throw new Error('例外') })
  .then(() => console.log('実行されない'),
        (error) => console.log(error.message)) // 例外 のみが表示される

Promise.allPromise.allSettledPromise.rase

複数のPromiseがあって、すべてresolveされてからthenを実行したいような場合、Promise.allを使う。

const p1 = new Promise((r) => setTimeout(() => { r(1); console.log(1) }, 1000))
const p2 = new Promise((r) => setTimeout(() => { r(2); console.log(2) }, 2000))
const p3 = new Promise((r) => setTimeout(() => { r(3); console.log(3) }, 3000))

Promise.all([p1, p2, p3]).then((values) => console.log(values))

// 実行結果
// 1
// 2
// 3
// [ 1, 2, 3 ]

しかし、Promise.allは一部でもrejectされるとthenではなくcatchが実行される。

const p1 = new Promise((r) => setTimeout(() => { r(1); console.log(1) }, 1000))
// rejectを実行している
const p2 = new Promise((_, r) => setTimeout(() => { r(2); console.log(2) }, 2000))
const p3 = new Promise((r) => setTimeout(() => { r(3); console.log(3) }, 3000))

Promise.all([p1, p2, p3])
       .then((values) => console.log(values))
       .catch((value) => console.log(value, 'in catch'))

// 実行結果。全てが終わるのを待たず、rejectされた時点でcatchが呼ばれている
// 1
// 2
// 2 in catch
// 3

rejectされたものがあっても、全てのPromiseが終わるのを待ってthenを実行したい場合はPromise.allSettledを使う。

const p1 = new Promise((r) => setTimeout(() => { r(1); console.log(1) }, 1000))
const p2 = new Promise((_, r) => setTimeout(() => { r(2); console.log(2) }, 2000))
const p3 = new Promise((r) => setTimeout(() => { r(3); console.log(3) }, 3000))

Promise.allSettled([p1, p2, p3])
       .then((values) => console.log(values))
       .catch((value) => console.log(value, 'in catch'))

// 実行結果
// 1
// 2
// 3
// [
//   { status: 'fulfilled', value: 1 },
//   { status: 'rejected', reason: 2 },
//   { status: 'fulfilled', value: 3 }
// ]
// このようにallSettledを利用した場合、
// 実行ステータスを含むオブジェクトの配列が引数に渡される

Promise.resolvePromise.reject

new Promise((r) => r(1))と書く代わりにPromise.resolve(1)と記述することができる。rejectの場合も同様にPromise.rejectを利用できる。

Promiseが解決すること

第一に、順序が保証されることがある。resolveされてからしか、thenは実行されないし、後続のthenも前のthenが終わってからしか実行されない。

もし、Promiseを使わずに非同期処理を直列に実行しようとした場合、コールバックをネストして渡すしか方法がなく非常に見通しが悪くなる。これが第二の解決点になる。

asyncawait

asyncasync () => {}のように関数やメソッドにつけ、その関数をPromiseを返す関数にする。

awaitawait Promiseインスタンスという書き方で、Promiseインスタンスの状態が確定するまで待つ。また、awaitasync関数の中でしか利用できない。

// Promiseを返す関数
function p() {
  return new Promise((r) => {
    console.log('start promise')
    setTimeout(() => {
      console.log('in setTimeout')
      r(10)
    }, 1000)
    console.log('end promise')
  })
}

// async 関数
async function a1(x) {
  console.log('start a1')
  // Promiseのインスタンスを指定してawait
  const value = await p()
  console.log('end a1')
  return x * value;
}

// async 関数
async function a2() {
  console.log('start a2')
  // async関数の戻り値(Promiseのインスタンス)を指定してawait
  const value = await a1(100)
  console.log(value)
  console.log('end a2')
}

a2()

簡単にいえば、asyncawaitnew Promiseのシンタックスシュガーのようなもので、いくつかの例外を除いて相互に書き換えができる。

function p() {
  return new Promise((r) => {
    console.log('start promise')
    setTimeout(() => {
      console.log('in setTimeout')
      r(10)
    }, 1000)
    console.log('end promise')
  })
}

function pa1(x) {
  return new Promise((r) => {
    console.log('start a1')
    p().then((value) => {
      console.log('end a1')
      r(x * value)
    })
  })
}

function pa2() {
  return new Promise((r) => {
    console.log('start a2')
    pa1(100).then((value) => {
      console.log(value)
      console.log('end a2')
    })
  })
}

pa2()

書き換えができないケースの例としては、上記のp関数のようにsetTimeoutのコールバック内でresolveされるようなケースがあり、こういったケースではPromiseで記述できてもasync/awaitで書き換えることはできない。