2022年6月18日

Promiseを使ったエラー処理

Promiseチェーンはエラー処理に非常に役立ちます。Promiseがリジェクトされると、制御は最も近いリジェクションハンドラにジャンプします。これは実際には非常に便利です。

例えば、以下のコードでは、fetch のURLが間違っており(そのようなサイトはありません)、.catch がエラーを処理しています。

fetch('https://no-such-server.blabla') // rejects
  .then(response => response.json())
  .catch(err => alert(err)) // TypeError: failed to fetch (the text may vary)

ご覧のとおり、.catch はすぐに現れる必要はありません。1つか2つの .then の後に現れることもあります。

あるいは、サイトはすべて正常でも、レスポンスが有効なJSONではないかもしれません。すべてのエラーをキャッチする最も簡単な方法は、チェーンの最後に .catch を追加することです。

fetch('/article/promise-chaining/user.json')
  .then(response => response.json())
  .then(user => fetch(`https://api.github.com/users/${user.name}`))
  .then(response => response.json())
  .then(githubUser => new Promise((resolve, reject) => {
    let img = document.createElement('img');
    img.src = githubUser.avatar_url;
    img.className = "promise-avatar-example";
    document.body.append(img);

    setTimeout(() => {
      img.remove();
      resolve(githubUser);
    }, 3000);
  }))
  .catch(error => alert(error.message));

通常、このような .catch は全くトリガーされません。しかし、上記のPromiseのいずれかがリジェクトした場合(ネットワークの問題、無効なJSONなど)、それをキャッチします。

暗黙的な try…catch

PromiseエグゼキュータとPromiseハンドラのコードには、周囲に「見えないtry..catch」があります。例外が発生すると、それはキャッチされ、リジェクションとして扱われます。

例えば、このコード

new Promise((resolve, reject) => {
  throw new Error("Whoops!");
}).catch(alert); // Error: Whoops!

は、これと全く同じように動作します

new Promise((resolve, reject) => {
  reject(new Error("Whoops!"));
}).catch(alert); // Error: Whoops!

エグゼキュータの周囲の「見えない try..catch」は、エラーを自動的にキャッチし、リジェクトされたPromiseに変換します。

これはエグゼキュータ関数だけでなく、そのハンドラでも同様です。.then ハンドラ内で throw すると、それはリジェクトされたPromiseを意味するため、制御は最も近いエラーハンドラにジャンプします。

例を示します。

new Promise((resolve, reject) => {
  resolve("ok");
}).then((result) => {
  throw new Error("Whoops!"); // rejects the promise
}).catch(alert); // Error: Whoops!

これは、throw 文によって発生したエラーだけでなく、すべてのエラーで発生します。例えば、プログラミングエラー

new Promise((resolve, reject) => {
  resolve("ok");
}).then((result) => {
  blabla(); // no such function
}).catch(alert); // ReferenceError: blabla is not defined

最後の .catch は、明示的なリジェクションだけでなく、上記のハンドラでの偶発的なエラーもキャッチします。

再スロー

すでに指摘したように、チェーンの最後にある .catchtry..catch と似ています。必要なだけ .then ハンドラを持つことができ、そのすべてでのエラーを処理するために最後に単一の .catch を使用できます。

通常の try..catch では、エラーを分析し、処理できない場合は再スローすることができます。同じことがPromiseでも可能です。

.catch 内部で throw を行うと、制御は次の最も近いエラーハンドラに移動します。そして、エラーを処理して正常に終了すると、次の最も近い成功した .then ハンドラに進みます。

以下の例では、.catch はエラーを正常に処理しています。

// the execution: catch -> then
new Promise((resolve, reject) => {

  throw new Error("Whoops!");

}).catch(function(error) {

  alert("The error is handled, continue normally");

}).then(() => alert("Next successful handler runs"));

ここで .catch ブロックは正常に終了します。そのため、次の成功した .then ハンドラが呼び出されます。

以下の例では、.catch のもう1つの状況を見てみましょう。ハンドラ (*) はエラーをキャッチしますが、それを処理できません (例えば、URIError のみを処理する方法を知っています)。そのため、再度スローします。

// the execution: catch -> catch
new Promise((resolve, reject) => {

  throw new Error("Whoops!");

}).catch(function(error) { // (*)

  if (error instanceof URIError) {
    // handle it
  } else {
    alert("Can't handle such error");

    throw error; // throwing this or another error jumps to the next catch
  }

}).then(function() {
  /* doesn't run here */
}).catch(error => { // (**)

  alert(`The unknown error has occurred: ${error}`);
  // don't return anything => execution goes the normal way

});

実行は、最初の .catch (*) からチェーンの下の次の (**) にジャンプします。

未処理のリジェクション

エラーが処理されない場合はどうなるでしょうか。例えば、以下のようにチェーンの最後に .catch を追加するのを忘れた場合

new Promise(function() {
  noSuchFunction(); // Error here (no such function)
})
  .then(() => {
    // successful promise handlers, one or more
  }); // without .catch at the end!

エラーが発生した場合、Promiseはリジェクトされ、実行は最も近いリジェクションハンドラにジャンプする必要があります。しかし、何もありません。そのため、エラーは「スタック」します。それを処理するコードはありません。

実際には、コードで通常発生する未処理のエラーと同様に、何かが非常にまずい状態になったことを意味します。

通常のエラーが発生し、try..catch でキャッチされないとどうなるでしょうか?スクリプトはコンソールにメッセージを表示して終了します。未処理のPromiseのリジェクションでも同様のことが起こります。

JavaScriptエンジンは、このようなリジェクションを追跡し、その場合にグローバルエラーを生成します。上記の例を実行すると、コンソールで確認できます。

ブラウザでは、unhandledrejection イベントを使用して、そのようなエラーをキャッチできます。

window.addEventListener('unhandledrejection', function(event) {
  // the event object has two special properties:
  alert(event.promise); // [object Promise] - the promise that generated the error
  alert(event.reason); // Error: Whoops! - the unhandled error object
});

new Promise(function() {
  throw new Error("Whoops!");
}); // no catch to handle the error

このイベントは、HTML標準の一部です。

エラーが発生し、.catch がない場合、unhandledrejection ハンドラがトリガーされ、エラーに関する情報を含む event オブジェクトを取得するため、何かを行うことができます。

通常、このようなエラーは回復不能であるため、最善の方法は、ユーザーに問題について通知し、おそらくインシデントをサーバーに報告することです。

Node.jsのような非ブラウザ環境では、未処理のエラーを追跡する他の方法があります。

まとめ

  • .catch は、reject() の呼び出しや、ハンドラでスローされたエラーなど、あらゆる種類のPromiseのエラーを処理します。
  • .then は、第2引数(エラーハンドラ)が指定されている場合、同じ方法でエラーをキャッチします。
  • エラーを処理し、それを処理する方法を知っている場所に正確に .catch を配置する必要があります。ハンドラはエラーを分析し(カスタムエラークラスが役立ちます)、不明なものを再スローする必要があります(プログラミングミスかもしれません)。
  • エラーから回復する方法がない場合は、.catch を全く使用しないでも構いません。
  • いずれにしても、未処理のエラーを追跡し、ユーザー(およびおそらくサーバー)に通知するために、unhandledrejection イベントハンドラ(ブラウザの場合、その他の環境の場合は同様のもの)が必要です。そうすることで、アプリが「単に終了」することがなくなります。

課題

どう思いますか?.catch はトリガーされるでしょうか?回答を説明してください。

new Promise(function(resolve, reject) {
  setTimeout(() => {
    throw new Error("Whoops!");
  }, 1000);
}).catch(alert);

答えは: いいえ、トリガーされません

new Promise(function(resolve, reject) {
  setTimeout(() => {
    throw new Error("Whoops!");
  }, 1000);
}).catch(alert);

章で述べたように、関数コードの周りに「暗黙的なtry..catch」があります。そのため、すべての同期エラーは処理されます。

しかし、ここでエラーが発生するのは、エグゼキュータが実行されているときではなく、後です。したがって、Promiseはそれを処理できません。

チュートリアルマップ

コメント

コメントする前に読んでください…
  • 改善点についてご提案がある場合は、コメントではなく、GitHub issueを送信するか、プルリクエストをお願いします。
  • 記事の内容が理解できない場合は、詳しく説明してください。
  • コードを少し挿入するには、<code> タグを使用し、複数行の場合は <pre> タグで囲み、10行を超える場合はサンドボックスを使用してください(plnkrjsbincodepen…)