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
は、明示的なリジェクションだけでなく、上記のハンドラでの偶発的なエラーもキャッチします。
再スロー
すでに指摘したように、チェーンの最後にある .catch
は try..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
イベントハンドラ(ブラウザの場合、その他の環境の場合は同様のもの)が必要です。そうすることで、アプリが「単に終了」することがなくなります。
コメント
<code>
タグを使用し、複数行の場合は<pre>
タグで囲み、10行を超える場合はサンドボックスを使用してください(plnkr、jsbin、codepen…)