本記事は、Valentino Gagliardi 氏の "A mostly complete guide to error handling in JavaScript." を許可を頂いた上で翻訳したものです。
TOC
- プログラミングにおけるエラーとは?
- JavaScript におけるエラーとは?
- JavaScript エラー型の種類
- 例外とは?
- 例外を投げると何が起きる?
- 同期的エラー処理
- 非同期エラー処理
- Node.js のエラー処理
- まとめ
プログラミングにおけるエラーとは?
私たちの書くプログラムは 常にうまく動作するわけではありません。
時に、プログラムを停止させたり、ユーザーに何か問題が起こったことを知らせたいシチュエーションがあります。
例えば、以下のようなケースがあるでしょう:
- プログラムが存在しないファイルを開こうとした
- ネットワークの接続が不調である
- ユーザーが無効な値を入力した
すべてのケースで、私たちがプログラマーとして、またはプログラミングエンジンを通して、 エラーを作成します。
エラーを作成することで、ユーザーに問題が起きたことをメッセージで伝えたり、プログラムの実行を停止させたりできるのです。
JavaScript におけるエラーとは?
JavaScript におけるエラーはオブジェクトです。このオブジェクトは、後にプログラムを停止するために 投げられるものです。
JavaScript で新しくエラーを作成するには、適切な コンストラクタ関数を呼び出します。例えば、一般的なエラーを新規に作成するには以下を実行します:
consterr=newError("Something bad happened!");
new
というキーワードを省略することもできます:
consterr=Error("Something bad happened!");
一度作成されると、エラーオブジェクトは3つのプロパティを提供します。
message
: エラーメッセージを含む文字列name
: エラーのタイプstack
: 関数実行のスタックトレース
例えば、適当なメッセージ文字列でTypeError
オブジェクトを作成した場合、message
は実際に渡した文字列となり、name
は"TypeError"
となります:
constwrongType=TypeError("Wrong type given, expected number");wrongType.message;// "Wrong type given, expected number"wrongType.name;// "TypeError"
Firefox は上記のプロパティの他に、columnNumber
、filename
、lineNumber
といった非標準プロパティを実装しています。
JavaScript エラー型の種類
JavaScript にはたくさんのエラー型があります。具体的には以下の通りです:
Error
EvalError
InternalError
RangeError
ReferenceError
SyntaxError
TypeError
URIError
これらのエラー型は、あたらしいエラーオブジェクトを返す 本物のコンストラクタ関数であることを忘れないでください。
あなた自身のエラーオブジェクトを作成する際、Error
と TypeError
という最も一般的な 2 つのエラー型を使うことが多いでしょう。
エラーの大多数は InternalError
や SyntaxError
のように、JavaScript エンジンから直接的に発現するものがほとんどです。
TypeError
の一例は、const
に再代入しようとした際に発生します:
constname="Jules";name="Caty";// TypeError: Assignment to constant variable.
SyntaxError
の一例は、タイプミスをしたときに発生します:
vax='33';// SyntaxError: Unexpected identifier
または、await
を async
関数以外で利用するなど、予約語を不適切な場所を使った場合にも発生します:
functionwrong(){await99;}wrong();// SyntaxError: await is only valid in async function
TypeError
の他の例としては、ページに存在しない HTML 要素を指定したときに発生します:
Uncaught TypeError: button is null
これらのよくあるエラーオブジェクトに加えて、AggregateError
オブジェクトが JavaScript にもうすぐ導入される予定です。後ほど見るように、AggregateError
は複数のエラーをまとめる際に便利です。
これらの組み込みエラーに加えて、ブラウザでは以下のようなもの目にすることがあります:
DOMException
DOMError
(Dupulicated, 今は使われていない)
DOMException
は Web APIs に関連するエラーファミリーです。ブラウザの中で、ばかげたことをしたときに投げられます。例えば以下のようなことです:
document.body.appendChild(document.cloneNode(true));
結果:
Uncaught DOMException: Node.appendChild: May not add a Document as a child
完全なリストは、MDNのこちらのページを参照してください。
例外とは?
多くのデベロッパーは、エラーと例外を同様のものとして考えています。実際には、 エラーオブジェクトが投げられたときにのみ、エラーオブジェクトが例外になるのです。
JavaScript で例外を投げるには、throw
とエラーオブジェクトを用います:
constwrongType=TypeError("Wrong type given, expected number");throwwrongType;
短縮形のほうがより一般的です。多くのコードベースで以下のようなものを目にするでしょう:
throwTypeError("Wrong type given, expected number");
または
thrownewTypeError("Wrong type given, expected number");
関数や条件分岐構文の外で例外を投げることはほとんどありません。代わりに、以下の例を考えてみましょう:
functiontoUppercase(string){if(typeofstring!=="string"){throwTypeError("Wrong type given, expected a string");}returnstring.toUpperCase();}
ここでは、関数の引数が文字列(string)かどうかをチェックしています。文字列でなければ、例外を投げます。
JavaScript のルール的には、エラーオブジェクトだけではなく何でも投げることができます:
throwSymbol();throw33;throw"Error!";thrownull;
しかしながら、 プリミティブ型を投げることは避け、適切はエラーオブジェクトを投げるべきです。
そうすることで、コードベースにおいてエラー処理の一貫性を保つことができます。他のチームメンバーがエラーオブジェクトにおいて error.message
や error.stack
にアクセスすることができます。
例外を投げると何が起きる?
例外はエレベーターが上に行くようなものです。一度例外を投げると、どこかで止められない限りプログラムスタックの中でぶくぶくと泡立ってしまします。
以下のようなコードを考えてみましょう:
functiontoUppercase(string){if(typeofstring!=="string"){throwTypeError("Wrong type given, expected a string");}returnstring.toUpperCase();}toUppercase(4);
このコードをブラウザもしくは Node.js で実行した場合、プログラムは停止し以下のようなエラーを表示します:
Uncaught TypeError: Wrong type given, expected a string
toUppercase http://localhost:5000/index.js:3
<anonymous> http://localhost:5000/index.js:9
さらに、エラーが発生した正確な行数を把握することができます。
この表示が スタックトレースであり、プログラムの問題を追跡する際に便利です。
スタックトレースは下から上に積み上がります。つまりここでは以下のようになっていました:
toUppercase http://localhost:5000/index.js:3
<anonymous> http://localhost:5000/index.js:9
ここから以下のことが言えます:
- 9 行目にあるプログラムの何かが
toUppercase
を呼び出した - 3 行目において
toUppercase
で問題が発生した
ブラウザのコンソールで確認する以外にも、エラーオブジェクトの stack
プロパティにアクセスすることによってスタックトレースを見ることができます。
もし例外が キャッチされなかった場合、つまり、プログラマが例外をキャッチするために何もしなかった場合、プログラムはクラッシュします。
コードの中で、いつ、どこで例外をキャッチするかは、その時々で異なります。
例えば、 プログラムを完全にクラッシュさせるために、例外をスタックに加えて伝播させたいかもしれません。これは、無効なデータで処理を進めるよりもプログラムを停止させたほうが安全である、といった、致命的なエラーを処理する際に起こりうることです。
さて、ここまでで基本の紹介をしたので、 JavaScript の同期処理と非同期処理における、エラーと例外処理に話を進めましょう。
同期的エラー処理
同期処理のコードはほとんどの場合単純でわかりやすいので、エラー処理も簡単です。
通常関数のエラー処理
同期処理のコードは、書かれた通りに順番に実行されます。前述のコードをもう一度見てみましょう:
functiontoUppercase(string){if(typeofstring!=="string"){throwTypeError("Wrong type given, expected a string");}returnstring.toUpperCase();}toUppercase(4);
ここで、JavaScript エンジンは toUppercase
を呼び出して実行しています。すべての処理は 同期的に行われます。このように同期関数から発生する例外を キャッチするには、try/catch/finally
を使うことができます:
try{toUppercase(4);}catch(error){console.error(error.message);// or log remotely}finally{// clean up}
通常、try
はハッピーパスや、潜在的に例外を投げる可能性のある関数呼び出しに対して利用します。
catch
は、 実際に例外を捉えます。エラーオブジェクトを受け取り、エラーの内容を検査することができます(そして本番環境ではログをリモートサーバーに送信したりします)。
一方で、finally
ステートメントは、関数の実行結果に関わらず実行されます。つまり、関数が失敗したか成功したかにかかわらず finally
内に書かれたコードは実行されます。
try/catch/finally
は 同期的な構造であることを覚えておいて下さい。そしていま、 非同期処理のコードから発生する例外をキャッチする方法を獲得したのです。
ジェネレーター関数のエラー処理
JavaScript におけるジェネレーター関数は、関数の特殊な形式です。
この形式の関数は、関数の内側のスコープとその外側の間で 双方向のコミュニケーションチャネルを提供する以外に、 任意に停止したり再開したりすることができます。
ジェネレーター関数を作成するには、function
キーワードの後ろにアスタリスク *
を付けます:
function*generate(){//}
そうすると、値を返すために関数内で yield
を使用することができます:
function*generate(){yield33;yield99;}
ジェネレーター関数の返り値は イテレータオブジェクトです。ジェネレーターから値を取り出すためには、2つの方法があります:
- イテレータオブジェクトの
next()
を呼び出す for...of
で イテレーションする
先程の例で、ジェネレーターから値を取り出す場合は、以下のようにできます:
function*generate(){yield33;yield99;}constgo=generate();
ここで go
がイテレータオブジェクトになります。
ここから、go.next()
を呼び出し、実行を進めることができます:
function*generate(){yield33;yield99;}constgo=generate();constfirstStep=go.next().value;// 33constsecondStep=go.next().value;// 99
ジェネレーターは、 呼び出し元から値や例外を受け取ることもできます。
next()
に加えて、ジェネレーターから返されたイテレータオブジェクトは、throw()
メソッドを持っています。
このメソッドを利用して、ジェネレーターに例外を注入することによってプログラムを停止させてみましょう:
function*generate(){yield33;yield99;}constgo=generate();constfirstStep=go.next().value;// 33go.throw(Error("Tired of iterating!"));constsecondStep=go.next().value;// never reached
注入された例外をキャッチするには、ジェネレーター関数内の処理を try/catch
構文で囲む必要があります(必要であれば finally
も利用できます):
function*generate(){try{yield33;yield99;}catch(error){console.error(error.message);}}
ジェネレーター関数は例外を関数の外に投げることもできます。この仕組みは、try/catch/finally
を使って同期処理の例外をキャッチするものと同じです。
ジェネレーター関数に対して for...of
構文を利用する例は以下のとおりです:
function*generate(){yield33;yield99;throwError("Tired of iterating!");}try{for(constvalueofgenerate()){console.log(value);}}catch(error){console.error(error.message);}/* Output:
33
99
Tired of iterating!
*/
ここでは、try
ブロックの中でハッピーパスを実行し、例外があれば catch
でキャッチします。
非同期エラー処理
JavaScript はシングルスレッドで実行されるプログラム言語であり、原理的には同期的です。
ブラウザエンジンのようなホスト環境が JavaScript の機能を拡張させたことで、外部のシステムと通信したり、I/O 処理を行うための、たくさんの Web API が使えるようになりました。
ブラウザにおける非同期性の例は タイムアウト(timeouts)、イベント(events)、プロミス(Promise)があります。
非同期の世界におけるエラー処理は同期の世界におけるそれとは異なります。
いくつか例を見ていきましょう。
タイマーのエラー処理
JavaScript を学び始めたばかりのとき、try/catch/finally
構文について学ぶと、あらゆるコードブロックを try/catch/finally
構文 で囲みたくなるかもしれません。
例えば以下のような関数を考えてみましょう:
functionfailAfterOneSecond(){setTimeout(()=>{throwError("Something went wrong!");},1000)}
この関数は、約 1 秒後にエラーを投げます。この例外を正しく扱うにはどうしたらよいでしょうか?
以下のようなコードは 上手く動きません :
functionfailAfterOneSecond(){setTimeout(()=>{throwError("Something went wrong!");},1000);}try{failAfterOneSecond();}catch(error){console.error(error.message);}
前述したように、try/catch
構文は同期的です。一方で、ここでは setTiemout
という、タイマー機能を持つブラウザの API を利用しています。
setTimeout
に渡したコールバック関数が実行されるときには、既にtry/catch
構文の実行は 終わっているのです。上のプログラムは例外をキャッチすることができず、クラッシュしてしまいます。
2 つの異なったトラックが実行されているのです:
Track A: --> try/catch
Track B: --> setTimeout --> callback --> throw
プログラムをクラッシュさせたくなければ、try/catch
構文を、setTimeout
に渡しているコールバック関数の中に移動する必要があります。
しかし、このアプローチは多くの場合意味を成しません。後で見るように、 Promises を用いた非同期エラー処理がより優れているのです。
イベントのエラー処理
Document Object Model (DOM) の HTML ノードは、EventTarget
と連携しています。EventTarget
は、ブラウザにおけるあらゆるイベントエミッターの共通の祖先といえる存在です。
これはつまり、ページ上の全ての HTML 要素におけるイベントを取得することができることを意味します。
(Node.js も今後のリリースで EventTarget
をサポートする予定です)
DOM イベントに対するエラー処理の仕組みは、非同期 Web API における仕組みと同様です。
以下の例を考えてみましょう:
constbutton=document.querySelector("button");button.addEventListener("click",function(){throwError("Can't touch this button!");});
ここでは、ボタンがクリックされた瞬間に例外を投げています。どのようにその例外をキャッチするのでしょうか?以下のパターンは 上手く動作せず、プログラムはクラッシュしてしまいます:
constbutton=document.querySelector("button");try{button.addEventListener("click",function(){throwError("Can't touch this button!");});}catch(error){console.error(error.message);}
setTimeout
の例で見たように、addEventListener
に渡されるあらゆるコールバック関数は、非同期的に実行されます:
Track A: --> try/catch
Track B: --> addEventListener --> callback --> throw
プログラムをクラッシュさせたくなければ、addEventListener
のコールバック関数内部に try/catch
構文を移動する必要があります。
しかしここでも、そのようにする意味がほぼありません。
setTimeout
の例で見たように、非同期処理コードの実行パスにおいて投げられた例外は 外側でキャッチすることができるものではなく、結果としてプログラムはクラッシュします。
次のセクションで、Promises と async/await
がどのように非同期処理におけるエラー処理を手軽なものにするか見ていきます。
onerror はどうだろう?
HTML 要素には、onlick
、onmouseenter
、onchange
など多くのイベントハンドラがあります。
そのなかには、onerror
もありますが、throw
やその類のものとは何も関係がありません。
onerror
イベントハンドラは、<img>
や <script>
のような HTML 要素が存在しないリソースを扱ったときにトリガーされます。
以下のような例を考えてみましょう:
// omitted
<body><imgsrc="nowhere-to-be-found.png"alt="So empty!"></body>
// omitted
上記のような、存在しないリソースを参照する要素を含んだ HTML ドキュメントをブラウザで見ると、コンソールに以下のようなエラーが表示されます:
GET http://localhost:5000/nowhere-to-be-found.png
[HTTP/1.1 404 Not Found 3ms]
JavaScript では、このエラーを以下のように「キャッチ」できるかもしれません:
constimage=document.querySelector("img");image.onerror=function(event){console.log(event);};
より優れた形で書くと、以下のようになります:
constimage=document.querySelector("img");image.addEventListener("error",function(event){console.log(event);});
このパターンは、画像やスクリプトなどのリソースに欠損があった際に、代替となるリソースをローディングしたい場合に便利です。
だたし、onerror
は throw
や try/catch
とは何の関係もないことを覚えておいて下さい。
Promise を用いたエラー処理
Promise によるエラー処理を説明するために、何度も登場している以下の例を「約束化(promisify)」させてみましょう。以下のコード例を編集していきます:
functiontoUppercase(string){if(typeofstring!=="string"){throwTypeError("Wrong type given, expected a string");}returnstring.toUpperCase();}toUppercase(4);
単純に文字列もしくは例外を返す代わりに、成功とエラーを処理するための Promise.reject
と Promise.resolve
を利用してみましょう:
functiontoUppercase(string){if(typeofstring!=="string"){returnPromise.reject(TypeError("Wrong type given, expected a string"));}constresult=string.toUpperCase();returnPromise.resolve(result);}
(厳密には、上記コードに非同期処理を行う部分はありませんが、説明するには十分です)
いま、toUppercase
関数は「約束」され、処理結果を扱うために then
を、 リジェクトされた Promise を処理するために catch
を使うことができます:
toUppercase(99).then(result=>result).catch(error=>console.error(error.message));
上記のコードは、以下のようなログを吐き出します:
Wrong type given, expected a string
Promise において、catch
はエラーを処理するための構成要素です。
catch
や then
に加え、finally
もあります。この finally
は、try/catch
構文における finally
と似たものです。
Promise における finally
も、返された Promise の結果に 関わらず実行されます:
toUppercase(99).then(result=>result).catch(error=>console.error(error.message)).finally(()=>console.log("Run baby, run"));
then/catch/finally
に渡されたコールバック関数は、Microtask キューによって非同期に処理されることを覚えておいて下さい。これらは、イベントやタイマーよりも優先される micro taskです。
プロミス(Promise)、エラー(error)そしてスロー(throw)
Promise をリジェクトする際は、引数としてエラーオブジェクト渡すのが ベストプラクティスです:
Promise.reject(TypeError("Wrong type given, expected a string"));
そうすることで、エラー処理の一貫性を保つことができます。他のチームメンバーが常に error.message
にアクセスすることができますし、さらに重要なことに、スタックトレースを調査することができます。
Promise.reject
に加えて、例外を投げることで Promise チェーンから抜け出すことができます。
以下のコード例を考えてみます:
Promise.resolve("A string").then(value=>{if(typeofvalue==="string"){throwTypeError("Expected a number!");}});
文字列を返すとともに Promise をリゾルブし、そしてその直後に throw
によって例外を投げています。
例外の伝播を食い止めるために、通常通り catch
を使うことができます:
Promise.resolve("A string").then(value=>{if(typeofvalue==="string"){throwTypeError("Expected a number!");}}).catch(reason=>console.log(reason.message));
このパターンは、fetch
を使う際によく用いられます。レスポンスオブジェクトのエラーチェックを行う例は以下の通りです:
fetch("https://example-dev/api/").then(response=>{if(!response.ok){throwError(response.statusText);}returnresponse.json();}).then(json=>console.log(json));
ここでも、catch
によって例外を受け取ることができます。もし例外を受け取ることに失敗した場合、あるいはあえて受け取らないことにした場合、 例外はキャッチされるまでスタックに残り続けます。
これは一概に悪いこととは言えませんが、環境によって、キャッチされていないリジェクトに対する挙動は異なります。
例えば、Node.js は将来的に、処理されていない Promise のリジェクトがあった場合は、プログラムをクラッシュさせる予定です:
DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
必ずリジェクトはキャッチしましょう!
"プロミス化"されたタイマーのエラー処理
タイマーとイベントにおいて、コールバック関数内で投げられた例外をキャッチすることは不可能ではありません。前のセクションで、以下のような例を挙げました:
functionfailAfterOneSecond(){setTimeout(()=>{throwError("Something went wrong!");},1000);}// DOES NOT WORKtry{failAfterOneSecond();}catch(error){console.error(error.message);}
Promise によって与えられた解決策は、コードの「プロミス化」です。基本的に、Promise でタイマーを囲みます:
functionfailAfterOneSecond(){returnnewPromise((_,reject)=>{setTimeout(()=>{reject(Error("Something went wrong!"));},1000);});}
reject
によって Promise のリジェクトをセットし、エラーオブジェクトを渡します。
この時点で、catch
をつかって例外を処理することができます:
failAfterOneSecond().catch(reason=>console.error(reason.message));
Tips: 成功した Promise の返り値の変数名として value
、Promise のリジェクトの変数名として reason
を使うことが一般的です。
Node.js は promisifyと呼ばれる、古い形で書かれたコールバック API をプロミス化するユーティリティを提供しています。
Promise.all のエラー処理
Promise の static メソッドである Promise.all
は Promise の配列を引数にとり、リゾルブした Promise の配列を返します。
constpromise1=Promise.resolve("All good!");constpromise2=Promise.resolve("All good here too!");Promise.all([promise1,promise2]).then((results)=>console.log(results));// [ 'All good!', 'All good here too!' ]
渡した配列のどれか1つでもリジェクトされた場合、Promise.all
は最初にリジェクトされた Promise のエラーとともにリジェクトします。
このような状況を扱うために、前のセクションで見たように catch
が利用できます:
constpromise1=Promise.resolve("All good!");constpromise2=Promise.reject(Error("No good, sorry!"));constpromise3=Promise.reject(Error("Bad day ..."));Promise.all([promise1,promise2,promise3]).then(results=>console.log(results)).catch(error=>console.error(error.message));
Promise.all
の実行結果に関わらず関数を実行するには、finally
を利用します:
Promise.all([promise1,promise2,promise3]).then(results=>console.log(results)).catch(error=>console.error(error.message)).finally(()=>console.log("Always runs!"));
Promise.any のエラー処理
Promise.any
(Firefox > 79, Chrome > 85) は、Promise.all
の反対の処理をする関数と考えることができます。
Promise.all
が、渡した配列の中に 1 つでもリジェクトされるものがあった場合にエラーを返すのに対し、Promise.any
はリジェクトが発生しても、リゾルブしたものが 1 つでもあればそれを返します。
Promise.any
に渡した配列に含まれる すべての Promiseがリジェクトされた場合、結果として得られるエラーは AggregatedError
です。以下のようなコード例を考えてみましょう:
constpromise1=Promise.reject(Error("No good, sorry!"));constpromise2=Promise.reject(Error("Bad day ..."));Promise.any([promise1,promise2]).then(result=>console.log(result)).catch(error=>console.error(error)).finally(()=>console.log("Always runs!"));
catch
を使ってエラーを処理しています。このコードの実行結果は以下の通りです:
AggregateError: No Promise in Promise.any was resolved
Always runs!
AggregatedError
オブジェクトは、通常の Error
オブジェクトと同様のプロパティに加えて、errors
プロパティを持っています:
//.catch(error=>console.error(error.errors))//
このプロパティは、それぞれのリジェクトで返されたエラーの配列を格納しています:
[Error: "No good, sorry!, Error: "Bad day ..."]
Promise.race のエラー処理
Promise.race
は、Promsie の配列を引数に取ります:
constpromise1=Promise.resolve("The first!");constpromise2=Promise.resolve("The second!");Promise.race([promise1,promise2]).then(result=>console.log(result));// The first!
得られる返り値は、 「レース」を制した 1 番着の Promiseです。
リジェクトされた場合はどうなるのでしょうか?リジェクトされる Promise が一番でなければ、Promise.race
はリゾルブします:
constpromise1=Promise.resolve("The first!");constrejection=Promise.reject(Error("Ouch!"));constpromise2=Promise.resolve("The second!");Promise.race([promise1,rejection,promise2]).then(result=>console.log(result));// The first!
もし リジェクトが一番になった場合、Promise.race
はリジェクトされ、以下のようにしてリジェクトをキャッチすることができます:
constpromise1=Promise.resolve("The first!");constrejection=Promise.reject(Error("Ouch!"));constpromise2=Promise.resolve("The second!");Promise.race([rejection,promise1,promise2]).then(result=>console.log(result)).catch(error=>console.error(error.message));// Ouch!
Promise.allSettled のエラー処理
Promise.allSettled
は ECMAScript 2020 で追加される関数です。
この関数を使って処理するケースはそれほど多くありません。なぜなら、 Promise のリジェクトがあったとしても、返り値は常にリゾルブされた Promise になるためです。
以下のようなコード例を考えてみます:
constpromise1=Promise.resolve("Good!");constpromise2=Promise.reject(Error("No good, sorry!"));Promise.allSettled([promise1,promise2]).then(results=>console.log(results)).catch(error=>console.error(error)).finally(()=>console.log("Always runs!"));
上の例では、リゾルブする Promise とリジェクトされる Promise を 1 つずつ含め、配列として渡しています。
このケースでは、catch
が実行されることはありません。代わりに finally
が実行されます。
then
内の処理によってロギングされる結果は次の通りです:
[{status:'fulfilled',value:'Good!'},{status:'rejected',reason:Error:Nogood,sorry!}]
async/await のエラー処理
JavaScript の async/await
は非同期関数を表しますが、コードを読む立場からみれば、同期関数の 可読性の高さの恩恵を受けているといえます。
話を単純にするために、何度も登場している同期関数 toUppercase
を、function
キーワードの前に async
を付け足すことで、非同期関数に変換します:
asyncfunctiontoUppercase(string){if(typeofstring!=="string"){throwTypeError("Wrong type given, expected a string");}returnstring.toUpperCase();}
async
というプレフィックスを使うことで、関数に Promise を返すように仕向けることができるようになります。これはつまり、then
や catch
、finally
といったチェーンが使えることを意味します:
asyncfunctiontoUppercase(string){if(typeofstring!=="string"){throwTypeError("Wrong type given, expected a string");}returnstring.toUpperCase();}toUppercase("abc").then(result=>console.log(result)).catch(error=>console.error(error.message)).finally(()=>console.log("Always runs!"));
async
関数内で例外を投げた場合、この例外は、 裏側で機能している Promise をリジェクトさせます。
どんなエラーも、catch
によってキャッチすることができます。
最も重要なことは、このスタイルに加えて同期関数と同様に try/catch/finally
構文を使える、ということです。
以下の例では、toUppercase
関数を consumer
という他の関数から呼び出しています。consumer
内部では、toUppercase
関数を try/catch/finally
関数で囲っています:
asyncfunctiontoUppercase(string){if(typeofstring!=="string"){throwTypeError("Wrong type given, expected a string");}returnstring.toUpperCase();}asyncfunctionconsumer(){try{awaittoUppercase(98);}catch(error){console.error(error.message);}finally{console.log("Always runs!");}}consumer();// Returning Promise ignored
実行結果は以下の通りです:
Wrong type given, expected a string
Always runs!
同様のトピックは次の記事でも扱っています: How to Throw Errors From Async Functions in JavaScript?
非同期ジェネレーターのエラー処理
JavaScript の 非同期ジェネレーターは、通常の値の代わりに Promise を yeild することができるジェネレーター関数です。
async
とジェネレーター関数を組み合わせて使います。イテレータオブジェクトが呼び出し元に対して Promise を返すジェネレーター関数です。
非同期ジェネレーターを作るために、async
でプレフィックスした、*
を持つ関数を定義します:
asyncfunction*asyncGenerator(){yield33;yield99;throwError("Something went wrong!");// Promise.reject}
Promise の仕組みに基づいているため、エラー処理に対しても同様のルールが適用されます。非同期ジェネレーター関数内の throw
は Promise のリジェクトに繋がり、catch
でキャッチすることができます。
非同期ジェネレーター関数から Promise を取り出すには、以下の 2 つのアプローチがあります。
then
ハンドラ- 非同期イテレーション
上のコード例では、最初の 2 つの値が yield
されたあとに、例外が投げられます。これは以下のようにできることを意味します:
constgo=asyncGenerator();go.next().then(value=>console.log(value));go.next().then(value=>console.log(value));go.next().catch(reason=>console.error(reason.message));
上記コードの実行結果は以下の通りです:
{ value: 33, done: false }
{ value: 99, done: false }
Something went wrong!
もう1つのアプローチは、 for await...of
で 非同期イテレーションを用いる方法です。非同期イテレーションを用いるためには、呼び出し側の関数を async
で囲む必要があります。
以下が完全なコード例です:
asyncfunction*asyncGenerator(){yield33;yield99;throwError("Something went wrong!");// Promise.reject}asyncfunctionconsumer(){forawait(constvalueofasyncGenerator()){console.log(value);}}consumer();
async/await
で見たように、潜在的に存在する例外は try/catch
で処理することができます:
asyncfunction*asyncGenerator(){yield33;yield99;throwError("Something went wrong!");// Promise.reject}asyncfunctionconsumer(){try{forawait(constvalueofasyncGenerator()){console.log(value);}}catch(error){console.error(error.message);}}consumer();
実行結果は以下の通りです:
3399Somethingwentwrong!
非同期ジェネレーター関数の返り値であるイテレータオブジェクトには、同期ジェネレーター関数と同様に throw()
メソッドがあります。
イテレータオブジェクトにおいて throw()
メソッドを呼び出すと、例外は投げず、代わりにリジェクトされた Promise を投げます:
asyncfunction*asyncGenerator(){yield33;yield99;yield11;}constgo=asyncGenerator();go.next().then(value=>console.log(value));go.next().then(value=>console.log(value));go.throw(Error("Let's reject!"));go.next().then(value=>console.log(value));// value is undefined
この状況を処理するには、以下のようにできます:
go.throw(Error("Let's reject!")).catch(reason=>console.error(reason.message));
ただし、イテレータオブジェクトの throw()
は ジェネレーター関数の内部に例外を送ることを忘れないでおきましょう。これは、以下のようなパターンを適用することを意味します:
asyncfunction*asyncGenerator(){try{yield33;yield99;yield11;}catch(error){console.error(error.message);}}constgo=asyncGenerator();go.next().then(value=>console.log(value));go.next().then(value=>console.log(value));go.throw(Error("Let's reject!"));go.next().then(value=>console.log(value));// value is undefined
Node.js のエラー処理
Node.js の同期エラー処理
Node.js における同期エラー処理は、今までみてきた内容とほとんど同じです。
同期コードには、try/catch/finally
が使えます。
しかしながら、非同期の世界に目を向けてみると、面白いことが起こります。
Node.js の非同期エラー処理: コールバックパターン
非同期コードにおいては、Node.js は 2 つの書き方に依存しています:
- コールバックパターン
- イベントエミッター
コールバックパターンにおいて 非同期 Node.js APIは、 イベントループを通して処理され コールスタックが空になるとすぐに実行されるという関数を引数に取ります。
以下のようなコードを考えてみましょう:
const{readFile}=require("fs");functionreadDataset(path){readFile(path,{encoding:"utf8"},function(error,data){if(error)console.error(error);// do stuff with the data});}
上のコードからコールバック関数を抽出すると、どのようにエラーを処理することになっているかを見ることができます:
//function(error,data){if(error)console.error(error);// do stuff with the data}//
fs.readFile
の実行過程においてエラーが発生した場合には、エラーオブジェクトを得ます。
この時点で、以下のことが可能です:
- 今までしてきたように、単純にエラーオブジェクトのログを表示する
- 例外を投げる
- 他のコールバックにエラーを渡す
例外を投げる場合は、以下のようにできます:
const{readFile}=require("fs");functionreadDataset(path){readFile(path,{encoding:"utf8"},function(error,data){if(error)throwError(error.message);// do stuff with the data});}
しかし、DOM におけるイベントやタイマーと同様に、この例外は プログラムをクラッシュさせます。以下のように try/catch
を用いてクラッシュを阻止しようとしても、うまくいきません:
const{readFile}=require("fs");functionreadDataset(path){readFile(path,{encoding:"utf8"},function(error,data){if(error)throwError(error.message);// do stuff with the data});}try{readDataset("not-here.txt");}catch(error){console.error(error.message);}
プログラムをクラッシュさせたくなければ、他のコールバックにエラーを渡すことが望ましい方法です。
const{readFile}=require("fs");functionreadDataset(path){readFile(path,{encoding:"utf8"},function(error,data){if(error)returnerrorHandler(error);// do stuff with the data});}
ここで用いている eventHandler
はその名前からも分かるように、エラーを処理するシンプルな関数です:
functionerrorHandler(error){console.error(error.message);// do something with the error:// - write to a log.// - send to an external logger.}
Node.js における非同期エラー処理: イベントエミッター
Node.js で行う多くのことは、 イベントに基づいています。ほとんどの場合、 エミッターオブジェクトと、いくつかのメッセージを待ち受けているオブザーバーとやり取りを行います。
Node.js のイベント駆動なモジュール(例えば netなど)はすべて EventEmitter
というルートクラスを継承しています。
Node.js の EventEmitter
は、2 つの基本的なメソッド持っています: on
と emit
です。
以下のような単純な HTTP サーバーを考えてみましょう:
constnet=require("net");constserver=net.createServer().listen(8081,"127.0.0.1");server.on("listening",function(){console.log("Server listening!");});server.on("connection",function(socket){console.log("Client connected!");socket.end("Hello client!");});
ここで、以下の 2 つのイベントを待ち受けています: listeningと connectionです。
これらのイベントに加えて、イベントエミッターはエラーが発生した際に エラーイベントも発火します。
もしこの上記コードのポート番号を 80 にして実行した場合、以下のような例外を得るでしょう:
constnet=require("net");constserver=net.createServer().listen(80,"127.0.0.1");server.on("listening",function(){console.log("Server listening!");});server.on("connection",function(socket){console.log("Client connected!");socket.end("Hello client!");});
実行結果:
events.js:291
throw er; // Unhandled 'error' event
^
Error: listen EACCES: permission denied 127.0.0.1:80
Emitted 'error' event on Server instance at: ...
この例外をキャッチするためには、 エラーを待ち受けるイベントハンドラを登録します:
server.on("error",function(error){console.error(error.message);});
以下のような結果を得ます:
listen EACCES: permission denied 127.0.0.1:80
さらに、プログラムはクラッシュしません。
このトピックについての詳細は、"Error Handling in Node.js" を読むと良いでしょう。
まとめ
このガイドでは、シンプルな同期コードから高度な非同期な仕組みまで、JavaScript のエラー処理全般を扱いました。
JavaScript のプログラムでは、例外の発生の仕方は多岐にわたります。
同期コードの例外は最も単純に対処することができますが、 非同期コードにおける例外処理は 複雑になる場合があります。
一方で、ブラウザの新しい JavaScript API はほとんどすべて Promise
に向かっています。普及したこのパターンは、then/catch/finally
または async/await
のtry/catch
を使って例外を処理することをより簡単にします。
このガイドを読んだ後は、 プログラムで起こり得るすべての状況を認識して、例外を正しくキャッチすることができるようになっているはずです。
最後までお読み頂きありがとうございました!