Pr

Promise の JavaScript

1個の演習

Promiseについて

Promiseオブジェクトは、非同期処理が最終的に完了すること(または失敗すること)と、その結果の値を表します。

Note

これは多くの人にとって難しいトピックです。特に、完全に_同期_の言語でプログラミングをしてきた人にとってはそうでしょう。 もし圧倒されてしまったり、並行性と並列性についてもっと学びたいと思ったら、素晴らしいトーク「Concurrency is not parallelism」を観る(go.dev経由)かVimeoで直接観る、そしてスライドを読むのがおすすめです。

プロミスのライフサイクル

Promiseには3つの状態があります:

  1. pending
  2. fulfilled
  3. rejected

プロミスは、作成された時点ではpendingです。 将来のある時点で、_resolve_または_reject_されることがあります。 一度resolveまたはrejectされると、そのプロミスが再びresolveやrejectされることはなく、状態が変わることもありません。

言い換えると、次のようになります:

  1. pendingのとき、プロミスは:
    • fulfilledまたはrejectedの状態に遷移することがあります。
  2. fulfilledのとき、プロミスは:
    • 他のどの状態にも遷移してはいけません。
    • 値を持たなければならず、その値は変わってはいけません。
  3. rejectedのとき、プロミスは:
    • 他のどの状態にも遷移してはいけません。
    • 理由を持たなければならず、その理由は変わってはいけません。

プロミスをresolveする

プロミスは、さまざまな方法でresolveできます:

// Creates a promise that is immediately resolved
Promise.resolve(value);

// Creates a promise that is immediately resolved
new Promise((resolve) => {
  resolve(value);
});

// Chaining a promise leads to a resolved promise
somePromise.then(() => {
  // ...
  return value;
});

上の例で、valueにはエラー、undefined、null、あるいは別のプロミスなど、_どんなもの_でも指定できます。 通常は、エラーではない値でresolveするのが望ましいでしょう。

プロミスをrejectする

プロミスは、さまざまな方法でrejectできます:

// Creates a promise that is immediately rejected
Promise.reject(reason)

// Creates a promise that is immediately rejected
new Promise((_, reject) {
  reject(reason)
})

// Chaining a promise with an error leads to a rejected promise
somePromise.then(() => {
  // ...
  throw reason
})

上の例で、reasonにはエラー、undefined、nullなど、_どんなもの_でも指定できます。 通常は、エラーでrejectするのが望ましいでしょう。

プロミスをチェーンする

プロミスは、resolveまたはrejectしたあとに、後続のアクションへと_つなげる_ことができます。

then

すべてのプロミスは「thenable」です。 つまり、元のプロミスがresolveされると実行されるthenという関数が利用できます。 promise.then(onResolved)の場合、コールバックonResolvedは、元のプロミスがresolveされたときの値を受け取ります。 thenは常に_新しい_「チェーンされた」プロミスを返します。

thenからvalueを返すと、「チェーンされた」プロミスはresolveします。 thenでreasonをthrowすると、「チェーンされた」プロミスはrejectします。

const promise1 = new Promise(function (resolve, reject) {
  setTimeout(() => {
    resolve('Success!');
  }, 1000);
});

const promise2 = promise1.then(function (value) {
  console.log(value);
  // expected output: "Success!"

  return true;
});

これは約1000ミリ秒後に"Success!"をログに出力します。 promise1の状態と値はresolvedと"Success!"になります。 promise2の状態と値はresolvedとtrueになります。

2つ目の引数を渡すこともでき、これは元のプロミスがrejectしたときに実行されます。 promise.then(onResolved, onRejected)の場合、コールバックonResolvedは元のプロミスがresolveされたときの値を受け取るか、コールバックonRejectedはプロミスがrejectされた理由を受け取ります。

const promise1 = new Promise(function (resolve, reject) {
  setTimeout(() => {
    resolve('Success!');
  }, 1000);

  if (Math.random() < 0.5) {
    reject('Nope!');
  }
});

function log(value) {
  console.log(value);
  return true;
}

function shout(reason) {
  console.error(reason.toUpperCase());
  return false;
}

const promise2 = promise1.then(log, shout);
  • 約半数のケースでは、約1000ミリ秒後に"Success!"をログに出力します。
    • promise1の状態と値はresolvedと"Success!"になります。
    • promise2の状態と値はresolvedとtrueになります。
  • 約半数のケースでは、すぐに"NOPE!"をログに出力します。
    • promise1の状態と値はrejectedとNope!になります。
    • promise2の状態と値はresolvedとfalseになります。

ライフサイクルのルール上、プロミスがrejectすると、約1000ミリ秒後にやってくるresolveは黙って無視されることを理解しておくことが大切です。一度rejectまたはresolveしたあとは、内部状態を変えられないからです。 プロミスから値を返すとそのプロミスはresolveし、値をthrowするとrejectすることも理解しておきましょう。 promise1がresolveし、チェーンされたonResolvedがある場合(then(onResolved))、その続きの処理は新しいプロミスで、resolveもrejectもできます。 promise1がrejectし、チェーンされたonRejectedがある場合(then(, onRejected))、その続きの処理は新しいプロミスで、resolveもrejectもできます。

catch

元のプロミスがrejectしたときだけエラーを捕捉して処理を続けたい、ということもあるでしょう。 promise.catch(onCatch)の場合、コールバックonCatchは元のプロミスがrejectされた理由を受け取ります。 catchは常に_新しい_「チェーンされた」プロミスを返します。

catchからvalueを返すと、「チェーンされた」プロミスはresolveします。 catchでreasonをthrowすると、「チェーンされた」プロミスはrejectします。

const promise1 = new Promise(function (resolve, reject) {
  setTimeout(() => {
    resolve('Success!');
  }, 1000);

  if (Math.random() < 0.5) {
    reject('Nope!');
  }
});

function log(value) {
  console.log(value);
  return 'done';
}

function recover(reason) {
  console.error(reason.toUpperCase());
  return 42;
}

const promise2 = promise1.catch(recover).then(log);

約半数のケースでは、約1000ミリ秒後に"Success!"をログに出力します。 残り半数のケースでは、すぐに42をログに出力します。

  • promise1がresolveすると、catchはスキップされ、thenに進んで値がログに出力されます。
    • promise1の状態と値はresolvedと"Success!"になります。
    • promise2の状態と値はresolvedと"done"になります。
  • promise1がrejectすると、catchが実行され、ここで_値が返される_ためチェーンはresolvedになり、thenに進んで値がログに出力されます。
    • promise1の状態と値はrejectedと"Nope!"になります。
    • promise2の状態と値はresolvedと"done"になります。

finally

プロミスがresolveするかrejectするかに関わらず、確定したあとにコードを実行したいこともあるでしょう。 promise.finally(onSettled)の場合、コールバックonSettledは何も受け取りません。 finallyは常に_新しい_「チェーンされた」プロミスを返します。

finallyからvalueを返すと、元のプロミスの状態と値がコピーされ、valueは無視されます。 finallyでreasonをthrowすると、「チェーンされた」プロミスはrejectし、元のプロミスの状態と値や理由は上書きされます。

例

いくつかのメソッドを組み合わせた例です:

const myPromise = new Promise(function (resolve, reject) {
  const sampleData = [2, 4, 6, 8];
  const randomNumber = Math.round(Math.random() * 5);

  if (sampleData[randomNumber]) {
    resolve(sampleData[randomNumber]);
  } else {
    reject('Sampling did not result in a sample');
  }
});

const finalPromise = myPromise
  .then(function (sampled) {
    // If the random number was 0, 1, 2, or 3, this will be
    // reached and the number 2, 4, 6, or 8 will be logged.
    console.log(`Sampled data: ${sampled}`);
    return 'yay';
  })
  .catch(function (reason) {
    // If the random number was 4 or 5, this will be reached and
    // reason will be "An error occurred". The entire chain will
    // then reject with an Error with the reason as message.
    throw new Error(reason);
  })
  .finally(function () {
    // This will always log after either the sampled data is
    // logged or the error is raised.
    console.log('Promise completed');
  });
  • randomNumberが0-3の場合:
    • myPromiseは値2, 4, 6, or 8でresolveされます
    • finalPromiseは値'yay'でresolveされます
    • 2つのログが出力されます:
      • Sampled data: ...
      • Promise completed
  • randomNumberが4-5の場合:
    • myPromiseは理由'Sampling did not result in a sample'でrejectされます
    • finalPromiseは理由Error('Sampling did not result in a sample')でrejectされます
    • 1つのログが出力されます:
      • Promise completed
      • 環境によっては、"uncaught rejected promise: Error('Sampling did not result in a sample')"というログが出力されます

上の例でわかるように、rejectは文字列を渡しても動作し、プロミスはErrorでrejectすることもできます。

Note

プロミスのチェーンや一般的な使い方がよくわからない場合は、MDNのチュートリアルが良い参考資料になります。

GitHubで編集 リンクは新しいウィンドウまたはタブで開きます

Promiseを学習する