読者です 読者をやめる 読者になる 読者になる
スポンサーリンク

JavaScriptでの例外設計 (throw,try-catch-finally構文のイメージと利用パターン)

javascript プログラミング


JavaScriptの例外(throw, try-catch-finally構文)の使い方について,以下の点を論じる。

  • (A)breakとreturnとthrowの比較
  • (B)throwを利用した設計のイメージ
  • (C)throwの使いどころ
  • (D)例外処理のサンプルコード×2

(A)break(continue)とthrowとreturnは何なのか

どれも「goto」である。


特に,breakがGOTOである事についてのサンプルコード:

HOGE:{

	alert( "このメッセージは表示されます。" );
	
	// "GOTO"の役目をする。該当ブロックから抜ける
	break HOGE;

	alert( "このメッセージは表示されません。" );

}// end of HOGE

alert( "HOGEブロックを抜けました。" );

http://d.hatena.ne.jp/GOLEM-XIV/20100427

  • breakはラベル付きループ,またはラベル付きブロックから抜ける。→ループでなくても利用可なので,GOTOとして使える。


以下はreturnとthrowの比較。

移動のスコープ:
  • returnは,必ずメソッドの階層を1段階戻す。「同じ関数内での移動」はできない。呼び出し元へ移動する。(クロージャを使えばある程度融通は利くが)
  • throwは,同一メソッド内でcatchしていれば,同一メソッド内での移動も可能。呼び出し元メソッドを辿っていき,どこかでcatchされていれば,そこに移動できる。メソッド呼び出しのスタック内の任意の階層に戻るようにさせることができる。
返り値:
  • returnは,何でも返す。返された値(例えば,エラーコードなど)の分類のためには,呼び出し側で自前でswitch文を書く必要がある。
  • throwは,エラー情報を返す事に特化した想定。Javaや,Firefox上でのJavaScriptであれば,構文そのものにcase文的な要素が織り込み済み。ただしIE/JScriptでは,catch節を多段書くことができないので,自前でcase文をcatch節内に書かなくてはならない。

Javaの場合
http://d.hatena.ne.jp/unageanu/200708...

  • 例外クラスを複数作るのではなくエラーコードで識別する,という戦略もあるにはある。
  • ※例外クラスの継承階層を適切に設計すれば(=個々のエラーを適切な粒度で再分類すれば),catch節が10個続くようなコードにはならない。


MDC:条件付catch節
https://developer.mozilla.org/ja/Core...

  • 1 つ以上の条件付き catch 節を使うこともできます。この場合、特定の例外が投げられると、適切な catch 節に入ります。
  • ECMAScript 仕様の一部ではありません。

以上,returnと比べた場合のthrow (try構文)のポイント。


(B)throwを利用した設計のイメージ

tryのイメージをさらに具体化する。

  • tryが書いてある上位処理 ←→ tryで包まれている下位処理

を,さまざまなアナロジーで表現してみよう。



日常生活に基づいたアナロジー:

  • 責任者 ←→ 無責任の作業者

  • 面倒を見てくれる人 ←→ 安心して失敗できる人

  • 親 ←→ 子

  • ガード ←→ 捨て駒

  • ジャッジ+主催者 ←→ 挑戦者

メソッド階層の実装のイメージ:

  • 呼び出し元 ←→ 呼び出し先
  • 処理が中断しない ←→ 処理が中断しうる
  • 異常系に対処する術を知っている人 ←→ 正常系だけ知っている作業者(をあらわすメソッド)
  • do_hoge_safe() ←→ do_hoge_unsafe()

  • Manager ←→ Worker
  • facade(順番に呼び出し) ←→ 個別function

(C)throwの使いどころ

JavaScriptでのコーディング時,どのような時にthrow / try-catchを使うのか。


(1)APIが返す場合。

JavaScriptのネイティブメソッド,例えばevalのエラーとか。

こういった関数が,処理の成功失敗のフラグを返すのではなく例外投げるだけという場合。

→catchを書かざるを得ない。


var str = "alert( 1 + );";

try
{
	eval( str );
}
catch( e )
{
	//alert( e.message );
	alert( "パースできません。" );
}
(2)自前の文字列の解析ロジック。(自前eval)

文字列のパースのアルゴリズムは異常系が多すぎるので予測しきれない。

すべての異常パターンをそれぞれif文のバリデーションで検知するのは非現実的。(=ブラックリストを作りきれない,ということ)

→こういう時に,異常系をDRYに書くためにtry-catch構文が役立つ。

正常系(ホワイトリストのようなもの)をtry節内に書き,だめだったときの処理はcatch内の一箇所にまとめる。

例外処理の設計
http://yakinikunotare.boo.jp/orebase/...

  • 例外を発生させないようにコードすると、コードのほとんどが例外条件を除外するif文の嵐に
  • 想定するオブジェクト状態以外を例外として処理
  • メソッドを使うたびにチェックロジックを書かなくていいので便利、DRY思想にも当てはまる
  • 電気回路でいうグランドのような一番外側に処理を捨てる場所
(3)正常系のコードがすでに一そろい動作している。それに手を加えないで,異常系にも対処したい。

「動いているコードに手を加えたくない」人の場合。


→外側にtryブロックで囲めばよい。try節内に手を加えなくてすむ。

あるいは,正常系のみを考慮したメソッドが完成している場合,それを呼び出すメソッドでtry-catchする。


(4)忘れてはならない必須の共通エラー処理や最終処理がある場合。

ファイル操作時は,ファイルの閉じ忘れに注意する。他の言語では例外をこういう時に使う。

JavaScriptでファイル操作は(ブラウザ上では)ありえないが,似たような状況はありえる。

サイ本(4th) try/catch/finallyの解説の末尾
http://docstore.mik.ua/orelly/webprog...

  • while文内でcontinueした時に,カウンターを上げ忘れると無限ループになる。それを防止するために,finally内にカウントアップを書く。
  • もしtry句内の処理が大きくて,continueをあちこちに書かれそうな場合,カウントアップの書き忘れも起きやすくなるから,この方法が有効。

→「共通の最終処理」を書きたい場合はfinallyを使う。


(5)ネットワーク通信時

ネットワーク処理のコードは,該当するプロトコル内でのやりとりを正常系で書いて積み上げる。

そのやり取りの途中で異常が発生したら,通信を途中で切り上げる(まさにGOTO)

「信頼性が低いが,再送はしやすい」というネットワーク特性がある場合は,確立済みコネクションの破棄をあまりためらわない。

前項の「ファイルの閉じ忘れ」と同じく,ソケットの閉じ忘れ対処。


→JavaScriptの場合は,Ajaxで例外処理が必須になる。
普段はライブラリがあるので意識しないが,XmlHttpRequestを扱うライブラリの中身を読むと勉強になる。


(6)呼び出し先と呼び出し元で分業しており,コミュニケーションをとりづらい時

→Exceptionの分類や振る舞いを,処理階層間のインタフェースとして取り決めておく。
Javaではふつうの作業だが,JavaScriptではそこまでやらないか。


(7)ループ文ではなくイテレータメソッドにしたのだがループを途中で止めたい場合

→breakの代替手段としてthrowを使う手が。

反復的なメソッドへの応用
http://ja.wikibooks.org/wiki/JavaScri...

(+α)例外を使わないほうがよい場合とは?

繰り返し実行され,速度が要求されるようなメソッドの場合。

効率的な JavaScript, ECMAScript
http://dev.opera.com/articles/view/ef...

  • 性能を決める関数で try-catch-finally を使うのはやめよう
  • catch節の初めと終わりには変数の生成・破棄処理が入るわけだが,「変数が実行時に生成, 破棄される」というのは言語の例外事項。そのため重くなる場合がある。
  • 例外処理はあまり通ることのないスクリプトの上のレベルに書いた方がいい.

これは,catchは下位レイヤではなく上位でするものだ,という普通の原則。




(D)サンプルコード1

異常系の対処コードを1行にまとめたようなサンプル。

// この行のコメントアウトを外すと,jsファイルを直接実行できます。
//function alert(s){ WScript.Echo( s ); }



// hogeが無い場合の例外クラス
var HogeNaiyoException = function(){
	// エラー発生時刻を記録
	this.error_happenned_at = new Date();
};
HogeNaiyoException.prototype = {
	message : "ハッシュ内にhogeがありませんでした。",
	rescue_me : function(){
		alert( "hogeが無かった場合の救済処置を実行しました。(通信とか)" );
	}
};


// fugaが無い場合の例外クラス
var FugaNaiyoException = function(){
	// エラー発生時刻を記録
	this.error_happenned_at = new Date();
};
FugaNaiyoException.prototype = {
	message : "ハッシュ内にfugaがありませんでした。",
	rescue_me : function(){
		alert( "fugaが無かった場合の救済処置を実行しました。(DOM操作とか)" );
	}
};



// メイン処理
var target_hash = {
	x : 1,
	y : 2,
	hoge : 3
};

try
{
	if( ! ( "hoge" in target_hash ) )
	{
		throw new HogeNaiyoException();
	}
	else if( ! ( "fuga" in target_hash ) )
	{
		throw new FugaNaiyoException();
	}
	
	// 〜
}
catch( e )
{
	// エラー内容のメッセージを表示
	alert( e.message );
	
	// エラー発生時刻を表示
	alert( e.error_happenned_at.toString() )
	
	// 発生したエラーの種類に応じて,救済処理を行なわせる
	e.rescue_me();
}

catch節内にポリモーフィズム「っぽさ」を感じる。


サンプルコード2:Firefox限定

Firefoxでは,catch節を多段にできる。

前掲のサンプルとcatch節だけ変えてある。

<script>

// hogeが無い場合の例外クラス
var HogeNaiyoException = function(){
	// エラー発生時刻を記録
	this.error_happenned_at = new Date();
};
HogeNaiyoException.prototype = {
	message : "ハッシュ内にhogeがありませんでした。",
	rescue_me : function(){
		alert( "hogeが無かった場合の救済処置を実行しました。(通信とか)" );
	}
};


// fugaが無い場合の例外クラス
var FugaNaiyoException = function(){
	// エラー発生時刻を記録
	this.error_happenned_at = new Date();
};
FugaNaiyoException.prototype = {
	message : "ハッシュ内にfugaがありませんでした。",
	rescue_me : function(){
		alert( "fugaが無かった場合の救済処置を実行しました。(DOM操作とか)" );
	}
};



// メイン処理
var target_hash = {
	x : 1,
	y : 2
};

try
{
	if( ! ( "hoge" in target_hash ) )
	{
		throw new HogeNaiyoException();
	}
	else if( ! ( "fuga" in target_hash ) )
	{
		throw new FugaNaiyoException();
	}
	
	// 〜
}
catch( e if e instanceof HogeNaiyoException )
{
	alert( e.message );
	e.rescue_me();
	alert( "hogeが無かった場合のエラー処理が終わりました。" );
}
catch( e if e instanceof FugaNaiyoException )
{
	alert( e.message );
	e.rescue_me();
	alert( "fugaが無かった場合のエラー処理が終わりました。" );
}
</script>


関連する記事:

JavaScriptで,クラスを継承する方法 (複数のサブクラスから共通クラスのプロトタイプを参照する)
http://language-and-engineering.hatenablog.jp/entry/20100924/p1


JavaScriptで,グローバル変数の存在判定をする3つの方法 ("window"の定義状況を確認したい)
http://language-and-engineering.hatenablog.jp/entry/20090412/p1


JavaScriptで,オブジェクトやクラスの初歩を理解しているか,実力を確かめるための7つの質問 (サンプルコード付き)
http://language-and-engineering.hatenablog.jp/entry/20100921/p1