正しいJavaScriptエラー処理

JavaScriptエラー処理は危険です。 マーフィーの法則を信じているなら、あなたは非常によく知っています:何かがうまくいかないことがあるなら、それはまさに起こることです! この記事では、JSでのエラー処理の落とし穴と適切なアプローチについて説明します。 最後に、非同期コードとAjaxについて説明しましょう。



JSイベントのパラダイムは、言語に特定の豊かさを追加すると信じています。 私は、エラーを含むイベント駆動型マシンとしてブラウザーを提示するのが好きです。 実際、間違いは何らかのイベントの非発生ですが、誰かがこれに同意することはありません。 この声明があなたにとって奇妙に思えるなら、あなたのシートベルトを締めてください;この旅行はあなたにとって珍しいでしょう。



すべての例は、クライアントJavaScriptを参照して検討されます。 この物語は、記事「 JavaScriptでの例外的なイベント処理 」で表明されたアイデアに基づいています 。 名前は次のように言い換えることができます。「例外が発生すると、JSは呼び出しスタック内のハンドラーをチェックします。」 基本的な概念に慣れていない場合は、まずその記事を読むことをお勧めします。 ここでは、例外を処理するための単純な要件に限らず、問題をより深く検討します。 そのため、次回try...catch



に再び遭遇したときは、目を覚ましてすでにアプローチしています。



デモコード



GitHubから例に使用されているコードをダウンロードできます、それはそのようなページです:







各ボタンをクリックすると、「爆弾」が爆発し、 TypeError



例外をシミュレートします。 以下は、単体テストからのこのモジュールの定義です。



 function error() { var foo = {}; return foo.bar(); }
      
      





最初に、関数は空のfoo



オブジェクトを宣言します。 どこにもbar()



定義がないことに注意してください。 単体テストを開始するときに爆弾がどのように爆発するかを見てみましょう。



 it('throws a TypeError', function () { should.throws(target, TypeError); });
      
      





テストはshould.jsのテストステートメントを使用してMochaで記述されています 。 Mochaはテストランナーとして機能し、should.jsはアサーションライブラリとして機能します。 これらのテストAPIにまだ出会っていない場合は、安全に学習できます。 テストの実行はit('description')



で始まり、 should



成功または失敗して終了します。 ブラウザを使用せずに、サーバーで直接実行できます。 テストを怠らないことをお勧めします。純粋なJavaScriptの重要なアイデアを証明できるからです。



そのため、 error()



最初に空のオブジェクトを定義してから、メソッドにアクセスしようとします。 ただし、 bar()



はオブジェクト内に存在しないため、例外がスローされます。 JavaScriptのような動的言語を使用している場合、これはすべての人に起こります!



不十分な取り扱い



誤ったエラー処理の例を考えてみましょう。 ハンドラーの開始をボタンのクリックにリンクしました。 単体テストでは次のようになります。



 function badHandler(fn) { try { return fn(); } catch (e) { } return null; }
      
      





依存関係として、ハンドラーはfn



コールバックを受け取ります。 次に、この依存関係はハンドラー関数内から呼び出されます。 単体テストはその使用を実証します:



 it('returns a value without errors', function() { var fn = function() { return 1; }; var result = target(fn); result.should.equal(1); }); it('returns a null with errors', function() { var fn = function() { throw Error('random error'); }; var result = target(fn); should(result).equal(null); });
      
      





ご覧のとおり、問題が発生した場合、この奇妙なハンドラはnull



返しnull



。 コールバックfn()



は、通常のメソッドまたは「爆弾」を示します。 物語の続き:



  (function (handler, bomb) { var badButton = document.getElementById('bad'); if (badButton) { badButton.addEventListener('click', function () { handler(bomb); console.log('Imagine, getting promoted for hiding mistakes'); }); } }(badHandler, error));
      
      





null



を取得することの何が問題になっていnull



か? これにより、エラーの原因が認識されず、有用な情報が提供されません。 このようなアプローチ-明確な通知なしで実行を停止する-は、UXの観点から誤った決定の原因となり、データ破損につながる可能性があります。 try...catch



を見下ろしながら、デバッグのために数時間殺すことができtry...catch



。 上記のハンドラーは、コード内のエラーを単純に飲み込み、すべてが正常であることを装います。 これは、コードの高品質をあまり気にしない企業で機能します。 ただし、将来エラーを非表示にすると、デバッグで一時的に大きな損失が発生することに注意してください。 コールスタックが深い多層製品では、問題の根本を見つけることはほとんど不可能です。 隠されたtry...catch



を使用するのが理にかなっている状況がいくつかありますが、これはエラー処理では避けるのが最善です。



明確な通知なしで実行の停止を使用する場合、最終的には、よりインテリジェントにエラー処理にアプローチする必要があります。 また、JavaScriptにより、よりエレガントなアプローチが可能になります。



曲線処理



続けましょう。 今度は、曲線エラーハンドラを検討します。 ここでは、DOMの使用については触れません。本質は前の部分と同じです。 曲がったハンドラーは、例外を処理する方法のみが悪いハンドラーと異なります。



 function uglyHandler(fn) { try { return fn(); } catch (e) { throw Error('a new error'); } } it('returns a new error with errors', function () { var fn = function () { throw new TypeError('type error'); }; should.throws(function () { target(fn); }, Error); });
      
      





悪いハンドラーと比較すると、間違いなく優れています。 例外により、呼び出しスタックがポップアップします。 ここでは、エラーがスタック巻き戻すという事実が気に入っています 。これはデバッグに非常に役立ちます。 例外が発生した場合、インタープリターは別のハンドラーを探してスタックを上に移動します。 これにより、コールスタックの最上部でエラーを処理する多くの機会が得られます。 しかし、これは曲がったハンドラであるため、元のエラーは単に失われます。 元の例外を見つけようとして、スタックに戻る必要があります。 例外をスローする問題があることを知っているのは良いことです。



湾曲したハンドラーからの害はそれほどありませんが、コードはまだ鈍くなっています。 ブラウザがそのための適切なエースを持っているかどうかを見てみましょう。



ロールバックスタック



コールスタックの先頭にtry...catch



を配置することにより、1つの方法で例外を解除できます。 例:



 function main(bomb) { try { bomb(); } catch (e) { // Handle all the error things } }
      
      





しかし、ブラウザはイベント駆動型です、覚えていますか? また、JavaScriptの例外は同じ完全なイベントです。 したがって、この場合、インタープリターは現在のコンテキストの実行を中断し、アンワインドを実行します。 グローバルonerror



イベントonerror



使用できます。これは次のようになります。



 window.addEventListener('error', function (e) { var error = e.error; console.log(error); });
      
      





このハンドラーは、実行可能なコンテキストでエラーをキャッチできます。 つまり、エラーが発生するとエラーイベントが発生する可能性があります。 ここでのニュアンスは、すべてのエラー処理がコード内の1箇所、つまりイベントハンドラーにローカライズされていることです。 他のイベントと同様に、特定のエラーに対処するハンドラーチェーンを作成できます。 SOLIDの原則を順守している場合は、各エラーハンドラーに独自の専門化を依頼できます。 ハンドラーはいつでも登録できます。インタープリターは必要な数だけループ内でハンドラーを実行します。 この場合、 try...catch



ブロックからコードベースを保存できます。これは、デバッグの恩恵を受けるだけです。 つまり、一番下の行は、イベント処理と同じ方法でJSのエラー処理にアプローチすることです。



グローバルハンドラーでスタックを巻き戻すことができるようになったので、この宝物で何をしますか?



スタックキャプチャ



呼び出しスタックは、問題を解決するための非常に便利なツールです。 特に、ブラウザーはそのままの状態で情報を提供するためです。 もちろん、エラーオブジェクトのstackプロパティは標準ではありませんが、最新バージョンのブラウザーでは一貫して使用可能です。



これにより、サーバーへのログインなどのクールなことができます。



 window.addEventListener('error', function (e) { var stack = e.error.stack; var message = e.error.toString(); if (stack) { message += '\n' + stack; } var xhr = new XMLHttpRequest(); xhr.open('POST', '/log', true); xhr.send(message); });
      
      





おそらく、これは上記のコードで目を引くことはありませんが、そのようなイベントハンドラーは上記のものと並行して動作します。 各ハンドラーは1つのタスクを実行するため、コードを記述するときはDRYの原則に従うことができます。



これらのメッセージがサーバーでどのようにキャッチされるかが好きです。







これは、Firefox Developer Edition 46からのメッセージのスクリーンショットです。正しいエラー処理のため、余分なものはなく、すべてが簡潔であり、要点です。 そして、間違いを隠す必要はありません! メッセージを見るだけで、誰がどこで例外をスローしたかがすぐにわかります。 このような透過性は、フロントエンドコードをデバッグするときに非常に役立ちます。 このようなメッセージは、エラーが発生する状況をよりよく理解するために、将来の分析のために永続ストアに保存できます。 一般に、デバッグニーズを含め、コールスタックの機能を過小評価しないでください。



非同期処理



JavaScriptは、現在の実行可能コンテキストから非同期コードを抽出します。 これは、以下のようなtry...catch



式で問題が発生することを意味します。



 function asyncHandler(fn) { try { setTimeout(function () { fn(); }, 1); } catch (e) { } }
      
      





単体テストバージョンに応じた開発:



 it('does not catch exceptions with errors', function () { var fn = function () { throw new TypeError('type error'); }; failedPromise(function() { target(fn); }).should.be.rejectedWith(TypeError); }); function failedPromise(fn) { return new Promise(function(resolve, reject) { reject(fn); }); }
      
      





ハンドラーをプロミス検証例外でラップする必要がありました。 素晴らしいtry...catch



周りにコードのブロックがあるにもかかわらず、未処理の例外があることに注意してください。 残念ながら、 try...catch



式は単一の実行可能コンテキストでのみ機能します。 そして、例外がスローされるまでに、インタープリターはすでにコードの別の部分に移動していたので、 try...catch



残しました。 Ajax呼び出しでもまったく同じ状況が発生します。



ここには2つの方法があります。 1つ目は、非同期コールバック内で例外をキャッチすることです。



 setTimeout(function () { try { fn(); } catch (e) { // Handle this async error } }, 1);
      
      





これは非常に有効なオプションですが、改善できる点がたくさんあります。 まず、 try...catch



ブロックはどこにでも散らばっています-1970年代のプログラミングへのオマージュです。 第二に、V8エンジン関数内でこれらのブロックをあまり使用しないため、開発者はtry...catch



を呼び出しスタックの上に配置することをお勧めします。



それで、私たちは何をしますか? グローバルエラーハンドラーが実行可能なコンテキストで機能することについては言及していません。 そのようなハンドラーがwindow.onerrorイベントに対して署名されている場合、他には何も必要ありません! すぐにDRYとSOLIDの原則に従うようになります。



以下は、例外ハンドラーによってサーバーに送信されるレポートの例です。 デモコードを実行する場合、使用しているブラウザによって、レポートが若干異なる場合があります。







このハンドラーは、エラーが非同期コード、より正確にはsetTimeout()



ハンドラーに関連していることも報告します。 ただのおとぎ話!



おわりに



エラー処理には、少なくとも2つの基本的なアプローチがあります。 1つは、エラーを無視して、通知なしで実行を停止する場合です。 2番目-エラーに関する情報をすぐに受け取り、発生するまで巻き戻します。 これらのアプローチのどれがより良いか、そしてなぜかは誰にでも明らかだと思います。 要するに、問題を隠さないでください。 プログラム障害の可能性について誰もあなたを責めることはありません。 実行を停止し、状態をロールバックし、ユーザーに新しい試行を与えることは完全に受け入れられます。 世界は不完全なので、二度目のチャンスを与えることが重要です。 間違いは避けられません。あなたがそれらにどう対処するかだけが重要です。



All Articles