【IRIS/Cache】エラートラップについて

本記事は、IRIS/Cacheにおけるエラートラップについて解説したいと思います。

※この記事は下記の方向けになります。
  • Try ~ Catchについて確認したい方
  • エラー処理を導入しているが、設計に悩む事がある方
  • Try ~ Catch は使ったことがあるが、従来のエラー処理との違いを知りたい方

はじめに

今更語る程でもないですが、「エラートラップ」はエラー発生時の状態を管理し、エラー状態から回復する処理です。

エラートラップでは、下記を重点的に対応すると考えます。

  • エラー原因の通知/対策・修正
  • 状況分析のため、エラー内容の記録
  • リソースの開放(ファイル・トランザクション等々)
  • エラー発生時の回復/対応とフローの変更

プログラムを作成している以上、エラーは付き物です。
エラーの無いプログラムを目指していますが、なかなか切っても切れない関係ですよね。

エラー処理を手厚く行っていれば、原因の特定も復旧も楽になると思います。

そのエラー処理で重要になるのは、今ではレガシーとなったエラートラップ機構の「$ztrap」「$etrap」と、現在の推奨となるTry-Catch構文になります。

本記事では、それぞれの違いや特徴を整理しながら、実際のコード例を交えて解説します。
 ※本記事はクラスでのエラー処理を中心に解説します。

準備:検証・確認用のクラス作成

先ずは、検証で使用するクラスを下記3つ用意します。
 ※ set文で引数を受け取ったり、executeで呼び出したりしています。

d ##class(developer.err.SampleA).testEcodeA()
 └ d ##class(developer.err.SampleB).testEcodeB()
   └ d ##class(developer.err.SampleC).testEcodeC()
     → s a = 1/0 ← <DIVIDE>エラー 発生用

Class developer.err.SampleA
{
ClassMethod testEcodeA()
{
	w "testEcodeA",!
	s txt = "s ret = ##class(developer.err.SampleB).testEcodeB()"
	x txt
}
}
Class developer.err.SampleB
{
ClassMethod testEcodeB()
{
	w "testEcodeB",!
	f pos=1:1:3 {
		d ##class(developer.err.SampleC).testEcodeC(pos)
	}
	q 1
}
}
Class developer.err.SampleC
{
ClassMethod testEcodeC(num As %Integer)
{
	w "testEcodeC",!
	i (num=3) {
		s a = 1/0
	}
}
}

準備が整ったら、検証を行いましょう。

Try ~ Cache

一先ずサンプルから確認します。
「準備」項目で紹介した関数を呼び出しています。

ClassMethod testTry()
{
	try {
		d ##class(developer.err.SampleA).testEcodeA()
		
	} catch e {
		// エラー処理
	}
}

tryブロック内でエラーが発生すると、catchブロック内の処理が実行されます。

またtryブロック内は下記特徴があります。

  • quitを実行すると、tryブロックから抜ける(quitに関数戻り値は設定できない)
  • tryブロック内はnewコマンドの範囲内ではない

エラー原因の取得方法

catchブロックでは、発生したエラーの原因を特定する必要があります。

発生したエラーの原因を特定する方法はいくつか存在しているので、解説を行います。

$zerror

特殊変数で、「エラーコード」「エラー発生のラベルとクラス(ルーチン)」「追加情報(一部処理のみ)」を取得する事が出来ます。

サンプル

} catch e {
    w $ZERROR
}

【実行結果】
<DIVIDE>testEcodeC+3^developer.err.SampleC.1

もっともシンプルですが、必用な情報はくみ取れるレベルだと思います。

$stack or $st

スタック情報を返します。

$stack($st)の仕様

取得方法説明
$st(level)実行コマンド (DO, $$, XECUTE等)
$st(level, “place”)最後に実行されたエントリ参照とコマンド番号を返す
$st(level, “mcode”)最後に実行されたコマンド等々を返す
$st(level, “ecode”)エラー・コードを返す

サンプル

	} catch e {
		s (txt, border) = ""
	  	f loop =0:1:$st(-1) {
		  	s mcode = $st(loop,"place")
		  	
		  	w !, txt, border, $s($st(loop)'="":"("_$st(loop)_") ", 1:"")
		  	i ($e(mcode)="@"){
			  	w $st(loop, "mcode")
		  	}else{
			  	s mcode = $st(loop,"place")
				w $s($e(mcode)=$c(9): $e(mcode,2,*), 1:mcode)
		  	}
			s txt = txt_"  "
			, border = "└ "
	  	}
	  	w ! ,txt, border, $st(loop, "mcode")
}

【実行結果】
D ##CLASS(developer.err.Sample).testTry()
  └ (DO) testTry+2^developer.err.Sample.1 +1
    └ (DO) testEcodeA+3^developer.err.SampleA.1 +1
      └ (XECUTE) s ret = ##class(developer.err.SampleB).testEcodeB()
        └ ($$) testEcodeB+3^developer.err.SampleB.1 +1
          └ (DO) testEcodeC+3^developer.err.SampleC.1 +1
            └ s a = 1/0

エラー箇所の他に、エラーが発生した処理の流れまで取得できるのが魅力です。

%Status ※e.AsStatus()

例外クラス(e)からステータスを取得し、そこから取得します・・・が、あまり有用ではないです。

サンプル

} catch e {
	s sts = e.AsStatus()
	w "-----",!
	f pos=$l(sts,$c(1)):-1:1{
		w $p(sts,$c(1),pos),!
	}
}

【実行結果】
—–
d^^^0
d^testTry+2^developer.err.Sample.1^1
x^testEcodeA+3^developer.err.SampleA.1^1&
e^testEcodeA+3^developer.err.SampleA.1^1*
d^testEcodeB+3^developer.err.SampleB.1^1*
^testEcodeC+3^developer.err.SampleC.1^1*
)
SAMPLEÖ

á

<DIVIDE>testEcodeC+3^developer.err.SampleC.1
Š.

0

大量の制御文字の中から、実行スタックの情報と「$zerror」の内容が含まれています。

この文字列から必用な情報を抽出するのは、あまり得策じゃないですね。

%SYSTEM.Statusの関数を利用する

%Status情報から、エラーコード等々を%SYSTEM.Status.clsから取得します。

サンプル

} catch e {
	s sts = e.AsStatus()
	d $system.Status.DisplayError(sts)
	w !,$system.Status.GetErrorCodes(sts)
	w !,$system.Status.GetErrorText(sts)
}

【実行結果】
エラー #5002: ObjectScript エラー:<DIVIDE>testEcodeC+3^developer.err.SampleC.1
5002
エラー #5002: ObjectScript エラー:<DIVIDE>testEcodeC+3^developer.err.SampleC.1

エラーコードと「$error」の内容を取得します。

状況によって、「DisplayError()」「GetErrorText()」を使いわけてください。

e.StackAsArray

例外クラスより、実行スタックの情報を配列で取得します。

サンプル

} catch e {
	s sts = e.AsStatus()
	d e.StackAsArray(.ary)
	f pos=1:1:ary {
		w ary(pos, "PLACE"),!
		w ary(pos), ?7
	}
}

【実行結果】
0
DO    testTry+2^developer.err.Sample.1 1
DO    testEcodeA+3^developer.err.SampleA.1 1
XECUTE testEcodeA+3^developer.err.SampleA.1 1
$$    testEcodeB+3^developer.err.SampleB.1 1
DO    testEcodeC+3^developer.err.SampleC.1 1

$stackとほぼ同じデータが取得できました。
足りないのは、エラーが発生したコマンド「s a = 1/0」くらいです。

e.DisplayString()

エラーの内容を出力します。

サンプル

} catch e {
	w e.DisplayString()
}

【実行結果】
<DIVIDE> 18 testEcodeC+3^developer.err.SampleC.1

シンプルなエラー情報です。
このコマンドを使用するなら、コーディング量の少ない$zerrorで十分な感じがします。

レガシー

今やレガシーとなった、旧来のエラートラップ方法ですがどこかで見るかもしれないので、備忘として動作を確認してみます。

$ZTRAP($zt)

$ztrapは特殊変数です。

クラスで使用する際は、プロシージャ(関数)の外にでる事が出来ません。
そのため、プロシージャ内にラベルを作成し、その中でエラー処理をコーディングする必要があります。

$ztrapのサンプル

ClassMethod testZTrap()
{
	new $etrap
	s $zt = "ErrProc" // エラー処理用ラベル
	d ##class(developer.err.SampleA).testEcodeA()
	q
ErrProc
  s $zt = ""
	// エラー処理
}

特殊変数「$ztrap」に、エラー発生時に遷移するラベル(ラベル名は任意)を設定します。

Try ~ Catchと異なり、エラー発生時に「%Exception.AbstractException.cls」関連のクラスを生成しません。

そのため、ラベル内で使用できるのは「$zerror」「$zstack」等になります。

$ETRAP

$etrapも特殊変数です。

$ztrapと異なるのは、クラスで使用する際プロシージャの外に出れる点です。

サンプルです

ClassMethod testEcode()
{
	new $et
	s et = "w !,""エラー発生!! ""_$zdt($h,3,1),!"
	, et = et_" s code = $st($st(-1),""mcode"")"
	, et = et_" d ##class(developer.err.ErrClass).PreErrProc()"
	, et = et_" d ##class(developer.err.ErrClass).ErrProc(code)"
	s $et = et
	
	d ##class(developer.err.SampleA).testEcodeA()
}

※エラー処理用のクラス

Class developer.err.ErrClass
{
ClassMethod PreErrProc()
{
	w "PreErrProc",!
}

ClassMethod ErrProc(code As %String)
{
	// $etrapは必ずクリアする
	s $et = ""
	
	// try-catch時に使用した$stackの流用+一部修正
	s (txt, border) = ""
	s max = $st(-1) -1
  	f loop =0:1:max {
	  	s mcode = $st(loop,"place")
	  	
	  	w !, txt, border, $s($st(loop)'="":"("_$st(loop)_") ", 1:"")
	  	i ($e(mcode)="@"){
		  	w $st(loop, "mcode")
	  	}else{
		  	s mcode = $st(loop,"place")
			w $s($e(mcode)=$c(9): $e(mcode,2,*), 1:mcode)
	  	}
		s txt = txt_"  "
		, border = "└ "
  	}
  	
  	w ! ,txt, border, code
}
}

特殊変数「$etrap」に、エラー発生時に遷移する処理を設定する点は、$ztrapと変わりません。

$ztrapと異なるのは、設定するエラー処理の自由度が高く、色々な処理を組み合わせる事が可能です。

この状態で処理を実行して、結果を参照してみます。

【実行結果】
testEcodeA
testEcodeB
testEcodeC
testEcodeC
testEcodeC

エラー発生!! 2025-03-28 14:34:24
PreErrProc

D ##CLASS(developer.err.Sample).testEcode()
  └ (DO) testEcode+6^developer.err.Sample.1 +1
    └ (DO) testEcodeA+3^developer.err.SampleA.1 +1
      └ (XECUTE) s ret = ##class(developer.err.SampleB).testEcodeB()
        └ ($$) testEcodeB+3^developer.err.SampleB.1 +1
          └ (DO) testEcodeC+3^developer.err.SampleC.1 +1
            └ s a = 1/0

writeコマンドや、2つの関数が正常に実行された事が分ります。

うん。$ztrapよりかは使いやすい。
・・・と思う。

とは言え、今となってはレガシーな技術なので、この2つの特殊変数を使用する事は、なかなか無さそうです。

速度検証

新旧のエラー処理で、処理速度がどの様に変化したのかも検証してみます。

ポイントとなるのは、エラーが発生しないケースでの処理時間ですよね。
エラーが発生した場合、Try-Cacheは「%Exception」パッケージをインスタンス化するので、レガシーより遅くなるのは予想できます。

今回比較を行うサンプルは下記になります。

ClassMethod speedCheck(cnt As %Integer)
{
	s start = $zh
	f pos=1:1:cnt d ..speedZtrap()
	w !,"$ztrap=",$zh-start
	
	s start = $zh
	f pos=1:1:cnt d ..speedEtrap()
	w !,"$etrap=",$zh-start
	
	s start = $zh
	f pos=1:1:cnt d ..speedTryCatch()
	w !,"try=",$zh-start
	
	s start = $zh
	f pos=1:1:cnt d ..speedNoErr()
	w !,"noerr=",$zh-start
}
// $ztrap
ClassMethod speedZtrap()
{
	s $ztrap="ErrProc"
	s a = 1/1
	q
ErrProc
	s $ztrap=""
	w !,"a"
	q
}
// $etrap
ClassMethod speedEtrap()
{
	new $etrap
	s $etrap="d ##class(developer.err.Sample).Err()"
	s a = 1/1
}
ClassMethod Err()
{
	s $etrap = ""
	w !,"b"
}
// Try-Catch
ClassMethod speedTryCatch()
{
	try {
		s a = 1/1
	} catch e {
		s sts = e.AsStatus()
		w !,"c"
	}
}
// 通常処理
ClassMethod speedNoErr()
{
	s a = 1/1
}

では、実行してみましょう。
各処理を1,000万回実行してみます[.speedCheck(10000000)]。

try-catchは、スタックされないので通常の処理と変わらないですね。
この結果からも、新しいtry-catch一択だと言うことが分かります。

$etrapは、newコマンドを実行する事で、$ztrapより遅くなっています。
 →newコマンドを除外すると、ほぼ変わらない処理時になります。。

エラー発生した際の処理時間を比較する

ついでにエラーが発生した場合の時間を計測してみたいと思います。
 →$ztrapとtry-catchの処理で「s a = 1/0」に変更します。

// $ztrap
ClassMethod speedZtrap()
{
	s $ztrap="ErrProc"
	s a = 1/0
	q
ErrProc
	s $ztrap=""
	q
}
// Try-Catch
ClassMethod speedTryCatch()
{
	try {
		s a = 1/0
	} catch e {
		s sts = e.AsStatus()
	}
}

では、実行してみましょう。
今回は、各処理を100万回で実行してみます[.speedCheck(1000000)]。

やはり、例外クラス(%Exception)をインスタンス化する分、エラー処理は遅いですね。
とは言え、エラーになる事の方が稀だと思うので、多少遅くても目をつぶっちゃいましょう。

おわりに

いかがだったでしょうか。

エラー処理は「保険」のようなもので、普段は目立たなくても、いざという時にシステムの安定性を左右する重要な要素です。

現在は、Try-Catchが優秀なので、そちらを主に運用していけば良いかと思います。
スマートなエラーハンドリングを目指していきましょう!

あなたのコードが、誰かの安心につながるように。
今日も丁寧に例外処理をしていきましょう。