JavaScriptの非同期処理で理解必須!Promiseの概要・挙動まとめ

JavaScript

JavaScriptで非同期処理を実装するにあたりPromiseの理解は重要です。
今回はPromiseの挙動についてまとめたので紹介をします。

Promiseについて

Promiseとは『状態』を持つオブジェクトです。オブジェクトの状態に応じてPromiseの挙動が決定します。

非同期処理をPromiseで実装すると、非同期処理の結果(成功 or 失敗)がPromiseオブジェクトの状態として表現されます。
つまり『非同期処理の完了を待ち、結果に応じた挙動を行う』ということがPromiseを利用することで実現可能になります。

Promiseで非同期処理を実装することで『コールバック地獄』や『非同期処理の例外ハンドリング』などの問題点が解決できます。

Promiseオブジェクトが持つ『状態』について

Promiseオブジェクトが持つ状態は以下の3つです。

Promiseオブジェクトが持つ状態
  • pending(保留)
  • fulfilled(成功)
  • rejected(失敗)

初期状態のpendingからfulfilledもしくはrejectedに状態が変化します。状態の変化は一度きりです。

Promiseのコンストラクタについて

Promiseのコンストラクタ(new Promise())では、第一引数にresolve()、第二引数にreject()を設定します。reject()は省略可能です。

resolve()は非同期処理が成功した場合に、reject()は失敗した場合に実行される関数です。

resolve()が実行されるとPromiseオブジェクトの状態がfulfilledになります。
一方、reject()が実行されるとPromiseオブジェクトの状態がrejectedになります。

たとえば『入力値を2倍にする(ただし、入力値が10以上だったら例外を発生させる)』というメソッドをPromiseで表現すると以下のようになります。

const double = (number) => {
  return new Promise((resolve, reject) => {
    (number < 10) ? resolve(number * 2) : reject(new Error(`${number}は10以上の数です`));
  })
}

失敗のケースを考慮しない、つまりreject()を省略した場合は以下のようになります。

const double = (number) => {
  return new Promise(resolve => resolve(number * 2))
}

コールバックを利用した非同期処理で実装した場合は以下のようになります。

const double = (number) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      (number < 10) ? resolve(number * 2) : reject(new Error(`${number}は10以上の数です`));
    }, 1000)
  })
}

Promiseのインスタンスメソッドについて

Promiseオブジェクトの結果はPromiseのインスタンスメソッドを利用して取得できます。

Promise.prototype.then()

then()には第一引数にPromiseオブジェクトの成功(fulfilled)の時、第二引数に失敗(rejected)の時に実行する関数を設定します。第二引数は省略可能です。

非同期処理をPromiseで実装する場合、『第一引数は非同期処理が成功した時、第二引数は失敗した時に実行する関数を設定する』と言い換えられます。

then()を利用したPromiseオブジェクトのデータ取得方法は以下の通りです。

const double = (number) => {
  return new Promise((resolve, reject) => {
    (number < 10) ? resolve(number * 2) : reject(new Error(`${number}は10以上の数です`));
  })
}

double(2).then(
  (result) => console.log(`Resut: ${result}`),
  (message) => console.log(`NG! ${message}`)
)
// Resut: 4


double(10).then(
  (result) => console.log(`Resut: ${result}`),
  (message) => console.log(`NG! ${message}`)
)
// NG! Error: 10は10以上の数です

なお、非同期処理の場合、then()はPromiseオブジェクトがpendingからfulfilledもしくはrejectedになるまで処理を実行しません。

Promise.prototype.catch()

catch()にはPromiseオブジェクトの失敗(rejected)の時に実行する関数を設定します。
つまりcatch()then()の第二引数の書き換え(糖衣構文、シンタックスシュガー)です。

catch()を利用したPromiseオブジェクトのデータ取得方法は以下の通りです。

const double = (number) => {
  return new Promise((resolve, reject) => {
    (number < 10) ? resolve(number * 2) : reject(new Error(`${number}は10以上の数です`));
  })
}

double(2)
  .then((result) => console.log(`Resut: ${result}`))
  .catch((message) => console.log(`NG! ${message}`))
// Resut: 4


double(10)
  .then((result) => console.log(`Resut: ${result}`))
  .catch((message) => console.log(`NG! ${message}`))
// NG! Error: 10は10以上の数です

then()・catch()のメソッドチェーンについて

then()catch()はPromiseオブジェクトを返すため、メソッドをつなげられます。メソッドをつなぐことをメソッドチェーンと呼びます。

先ほどのメソッドをthen()でつなぐと以下のようになります。なお、then()の結果は次のthen()の引数に渡されます。

const double = (number) => {
  return new Promise((resolve, reject) => {
    (number < 10) ? resolve(number * 2) : reject(new Error(`${number}は10以上の数です`));
  })
}

const onFulfilledResult = (result) => {
  console.log(`Resut: ${result}`)
  return result
}

double(2)
  .then(onFulfilledResult) // Resut: 4
  .then(double) // (前段のthenのreturn 4を引数に利用)
  .then(onFulfilledResult) // Resut: 8

rejectedになった場合はcatch()が実行されます。

const double = (number) => {
  return new Promise((resolve, reject) => {
    (number < 10) ? resolve(number * 2) : reject(new Error(`${number}は10以上の数です`));
  })
}

const onFulfilledResult = (result) => {
  console.log(`Resut: ${result}`)
  return result
}

const onRejectedMessage = (message) => {
  console.log(`NG! ${message}`)
}

double(3)
  .then(onFulfilledResult) // Resut: 6
  .then(double) // (前段のthenのreturn 6を引数に利用)
  .then(onFulfilledResult) // Resut: 12
  .then(double) // (前段のthenのreturn 12を引数に利用)
  .catch(onRejectedMessage) // NG! Error: 12は10以上の数です

Promiseの静的メソッド

Promiseで用意されている静的メソッドの活用方法について紹介します。

Promise.resolve()

Promise.resolve()fulfilledなPromiseオブジェクトを返します。
つまり、Promiseコンストラクタにおけるreturn resolve()の書き換えです。

以下の2つの関数メソッドは等価です。

// Promiseコンストラクタを利用する場合
const double = (number) => {
  return new Promise(resolve => resolve(number * 2))
}

// Promise.resolveで書き換えた場合
const double = (number) => {
  return Promise.resolve(number * 2)
}

Promise.resolve()then()をつなげることで、then()で設定された関数が実行されます。

const onFulfilledResult = (result) => {
  console.log(`Resut: ${result}`)
  return result
}

Promise.resolve(6).then(onFulfilledResult) // Resut: 6

Promise.reject()

Promise.reject()rejectedなPromiseオブジェクトを返します。
つまり、Promiseコンストラクタにおけるreturn reject()の書き換えです。

以下の2つの関数メソッドは等価です。

// Promiseコンストラクタを利用する場合
const double = (number) => {
  return new Promise((resolve, reject) => {
    (number < 10) ? resolve(number * 2) : reject(new Error(`${number}は10以上の数です`));
  })
}

// Promise.resolve, Promise.rejectで書き換えた場合
const double = (number) => {
  return (number < 10) ? Promise.resolve(number * 2) : Promise.reject(new Error(`${number}は10以上の数です`));
}

Promise.reject()catch()メソッドをつなげることで、catch()で設定された関数が実行されます。

const onRejectedMessage = (message) => {
  console.log(`NG! ${message}`)
}

Promise.reject(20).catch(onRejectedMessage) // NG! 20

Promise.all()

Promise.all()は複数の非同期処理を同時実行する場合に利用します。

指定したすべてのPromiseオブジェクトがfulfilledになるのを待ってから、成功の処理を実行します。
指定したPromiseオブジェクトのうち、ひとつでもrejectedであれば失敗の処理が実行されます。

const double = (number) => {
  return new Promise((resolve, reject) => {
    (number < 10) ? resolve(number * 2) : reject(new Error(`${number}は10以上の数です`));
  })
}

const doubleSlow = (number) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      (number < 10) ? resolve(number * 2) : reject(new Error(`${number}は10以上の数です`));
    }, 1000)
  })
}

const onFulfilledResult = (result) => {
  console.log(`Resut: ${result}`)
  return result
}

const onRejectedMessage = (message) => {
  console.log(`NG! ${message}`)
}

Promise.all([double(2), doubleSlow(3)])
  .then((receivedList) => {
    onFulfilledResult(receivedList[0]), onFulfilledResult(receivedList[1])
  })
  .catch(onRejectedMessage)
// Resut: 4
// Resut: 6

Promise.all([double(2), doubleSlow(10)])
  .then((receivedList) => {
    onFulfilledResult(receivedList[0]), onFulfilledResult(receivedList[1])
  })
  .catch(onRejectedMessage)
// NG! Error: 10は10以上の数です

Promise.all()はたとえば『複数のAjax通信が完了してから特定の処理を行う』といったケースで利用されます。

Promise.race()

Promise.race()Promise.all()同様、複数の非同期処理を同時実行する場合に利用します。

指定したPromiseオブジェクトのうち、いずれかの状態が決定したタイミングで成功もしくは失敗の処理が実行されます。

const double = (number) => {
  return new Promise((resolve, reject) => {
    (number < 10) ? resolve(number * 2) : reject(new Error(`${number}は10以上の数です`));
  })
}

const doubleSlow = (number) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      (number < 10) ? resolve(number * 2) : reject(new Error(`${number}は10以上の数です`));
    }, 1000)
  })
}

const onFulfilledResult = (result) => {
  console.log(`Resut: ${result}`)
  return result
}

const onRejectedMessage = (message) => {
  console.log(`NG! ${message}`)
}

Promise.race([double(2), doubleSlow(3)])
  .then(onFulfilledResult)
  .catch(onRejectedMessage)
// Resut: 4

Promise.race([double(10), doubleSlow(2)])
  .then(onFulfilledResult)
  .catch(onRejectedMessage)
// NG! Error: 10は10以上の数です

Promise.race([double(2), doubleSlow(10)])
  .then(onFulfilledResult)
  .catch(onRejectedMessage)
// Resut: 4

Promise.allSettled()

Promise.allSettled()Promise.all()同様、すべてのPromiseオブジェクトの結果を待ちます。
Promise.all()と異なり、いずれかのPromiseオブジェクトがrejectedになってもPromise.allSettled()自身は失敗とはなりません。

const double = (number) => {
  return new Promise((resolve, reject) => {
    (number < 10) ? resolve(number * 2) : reject(new Error(`${number}は10以上の数です`));
  })
}

const doubleSlow = (number) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      (number < 10) ? resolve(number * 2) : reject(new Error(`${number}は10以上の数です`));
    }, 1000)
  })
}

Promise.allSettled([double(10), doubleSlow(2)])
  .then(results => results.forEach( result => console.log(result)))
  .catch(results => results.forEach( result => console.log(result)))
//   {
//   status: 'rejected',
//   reason: Error: 10は10以上の数です
// }
// { status: 'fulfilled', value: 4 }

まとめ

Promiseまとめ
  • Promiseオブジェクトはpending、fulfilled、rejectedの3つの状態を持つ
  • Promiseオブジェクトはresolve()でfulfilledになり、then()でデータを取得する
  • Promiseオブジェクトはreject()でrejectedになり、catch()もしくはthen()の第二引数でデータを取得する
  • Promise.all()はすべてのPromiseの実行完了を待ってから処理をする
  • Promise.race()はいずれかのPromiseの実行が完了した時点で処理をする
  • Promise.allSettled()はPromiseの実行結果にかかわらず、すべてのPromiseオブジェクトの処理をする

Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!