接続/エクスプレスで要求コンテキストを維持しながら非同期エラーを処理する

node.jsで多かれ少なかれ大きなWebプロジェクトを開発しなければならなかった人々は、おそらく非同期呼び出しの内部で発生したエラーを処理する問題に直面していました。 通常、この問題はすぐには発生せず、「Hello、World!」以上のことを行う多くのコードが既に作成されている場合に発生します。



問題の本質



たとえば、単純な接続アプリケーションを使用します。



var connect = require('connect'); var getName = function () { if (Math.random() > 0.5) { throw new Error('Can\'t get name'); } else { return 'World'; } }; var app = connect() .use(function (req, res, next) { try { var name = getName(); res.end('Hello, ' + name + '!'); } catch (e) { next(e); } }) .use(function (err, req, res, next) { res.end('Error: ' + err.message); }); app.listen(3000);
      
      





ここに、ある程度の確率でエラーを生成する同期関数があります。 このエラーをキャッチして、一般的なエラーハンドラーに渡し、ユーザーにエラーを表示します。 この例では、関数は同期的に呼び出され、エラー処理は他の言語の同様のタスクと変わりません。



ここで同じことを試してみましょうが、getName関数は非同期になります。



 var connect = require('connect'); var getName = function (callback) { process.nextTick(function () { if (Math.random() > 0.5) { callback(new Error('Can\'t get name')); } else { callback(null, 'World'); } }); }; var app = connect() .use(function (req, res, next) { getName(function(err, name) { if (err) return next(err); res.end('Hello, ' + name + '!'); }); }) .use(function (err, req, res, next) { res.end('Error: ' + err.message); }); app.listen(3000);
      
      





この例では、try / catchを使用してエラーをキャッチできなくなりました。 関数呼び出し中には発生しませんが、後で発生する非同期呼び出し内(この例では、イベントループの次の反復)に発生します。 したがって、node.jsの開発者が推奨するアプローチを使用しました。コールバック関数の最初の引数にエラーを渡します。



このアプローチは、非同期呼び出し内のエラー処理の問題を完全に解決しますが、そのような呼び出しが多数ある場合、コードを大幅に増大させます。 実際のアプリケーションでは、互いに呼び出し、ネストされた呼び出しを持つことができ、非同期呼び出しのチェーンの一部である多くのメソッドが表示されます。 また、コールスタックのどこかでエラーが発生するたびに、エラーを最上部に「配信」する必要があります。そこでエラーを正しく処理し、ユーザーに緊急事態を通知できます。 同期アプリケーションでは、try / catchがこれを行います-入れ子になった複数の呼び出し内でエラーをスローし、呼び出しスタックに手動で渡すことなく、正しく処理できる場所でキャッチできます。



解決策





バージョン0.8.0から、 Domainと呼ばれるメカニズムがNode.JSに登場しました。 process.on( 'uncaughtException')とは異なり、実行コンテキストを保持しながら、非同期呼び出し内のエラーをキャッチできます。 ここでドメインに関するドキュメントをもう一度言うのは意味がないと思います。 その動作のメカニズムは非常に単純なので、すぐにconnect / expressのユニバーサルエラーハンドラーの特定の実装に進みます。



Connect / Expressはすべてのミドルウェアをtry / catchブロックでラップするため、ミドルウェア内にスローすると、エラーはエラーハンドラーのチェーン(4つの入力引数を持つミドルウェア)に渡され、そのようなミドルウェアがない場合はデフォルトのエラーハンドラーに渡されますトレースエラーをブラウザとコンソールに出力します。 ただし、この動作は、同期コードで発生するエラーにのみ関係します。



ドメインの助けを借りて、リクエストのコンテキストで非同期呼び出しの内部で発生したエラーを、このリクエストのエラーハンドラのチェーンにリダイレクトできます。 現在、最終的には、同期エラーと非同期エラーの処理は同じように見えます。



この目的のために、この問題を解決する接続/エクスプレス用の小さなミドルウェアモジュールを作成しました。 モジュールはGitHubおよびnpmで利用可能です。



使用例:



 var connect = require('connect'), connectDomain = require('connect-domain'); var app = connect() .use(connectDomain()) .use(function(req, res){ if (Math.random() > 0.5) { throw new Error('Simple error'); } setTimeout(function() { if (Math.random() > 0.5) { throw new Error('Asynchronous error from timeout'); } else { res.end('Hello from Connect!'); } }, 1000); }) .use(function(err, req, res, next) { res.end(err.message); }); app.listen(3000);
      
      





この例では、同期呼び出しと非同期呼び出しの内部でスローされたエラーは同じ方法で処理されます。 リクエストのコンテキスト内の任意の呼び出し深度でエラーをスローできます。エラーは、このリクエストのエラーハンドラのチェーンによって処理されます。



 var connect = require('connect'), connectDomain = require('connect-domain'); var app = connect() .use(connectDomain()) .use(function(req, res){ if (Math.random() > 0.5) { throw new Error('Simple error'); } setTimeout(function() { if (Math.random() > 0.5) { process.nextTick(function() { throw new Error('Asynchronous error from process.nextTick'); }); } else { res.end('Hello from Connect!'); } }, 1000); }) .use(function(err, req, res, next) { res.end(err.message); }); app.listen(3000);
      
      







結論として、執筆時点でのDomainモジュールの安定性はまだ実験的なものであることに注意してください。ただし、小規模なプロダクションでも問題はないとしても、既に説明したアプローチを使用しています。 このモジュールを使用しているサイトは決してクラッシュせず、メモリリークの影響を受けません。 1か月以上の稼働時間プロセス。



All Articles