実務中級者向け:VB.NETでのカスタム例外の作成とスロー:業務エラーコードや追加情報を保持する独自の例外設計の極意
数々のレガシーシステムを現代の.NETへと昇華させてきたアーキテクトであれば、一度は痛感したことがあるはずだ。
「`Exception`をそのままcatchし、メッセージ文字列だけで業務エラーを判別する」という悪夢のような設計の愚かしさを。
システムエラー(DB接続断、メモリ不足、NullReferenceException等の想定外のバグ)と、業務エラー(残高不足、入力値の桁数オーバー、ステータス不整合などの仕様上の弾き)を同一の例外クラスで扱っている現場は、いまだに後を絶たない。これでは、上位レイヤーでの適切なリカバリや、国際化(多言語対応)、ログの構造化解析において致命的なボトルネックとなる。
今回は、VB.NET環境において、業務エラーコードや構造化された追加情報を完全にカプセル化し、堅牢かつ保守性の高いシステムを構築するための「カスタム例外設計の極意」を授けよう。
—
1. 業務例外設計における「3大アンチパターン」
まず、アマチュアプログラマーが陥りがちな設計の罠を断ち切る。
1. 文字列パースによるエラー判定
`If ex.Message.Contains(“残高不足”)` などと書くコードは、UIの文言変更一つでシステム全体を崩壊させる。エラーは「コード(識別子)」で判定すべきである。
2. Exceptionの乱用と不適切な握り潰し
`Catch ex As Exception` でキャッチし、単にログを出してスルーする、あるいは無意味にラップし直すことで、スタックトレースの元情報(InnerException)を失う。
3. シリアライズ考慮漏れの設計
特にWCFや古いRemoting、あるいはAppDomainを跨ぐアーキテクチャ、またはマイクロサービス間のシリアライズにおいて、カスタム例外が `ISerializable` を正しく実装していないためにデシリアライズ時にクラッシュする。
これらを完全に排除し、エンタープライズグレードに耐えうるカスタム例外クラスをVB.NETで実装する。
—
2. 堅牢なカスタム例外クラスの実装コード
以下のコードは、.NET Framework / .NET Core (.NET 6/7/8) の双方で通用し、エラーコード、詳細な追加データ(辞書型)、そして完全なシリアライズサポートを備えた実戦投入可能な最高峰のカスタム例外のテンプレートである。
Option Strict On
Option Explicit On
Imports System
Imports System.Collections.Generic
Imports System.Runtime.Serialization
”’
”’ エラーコードと構造化された追加情報を保持し、上位レイヤーでの厳密なハンドリングを可能にする。
”’
Public Class BusinessLogicException
Inherits Exception
‘ —————————————————————-
‘ プロパティ定義
‘ —————————————————————-
”’
”’
Public ReadOnly Property ErrorCode As String
”’
”’
Public ReadOnly Property ErrorDetails As Dictionary(Of String, String)
‘ —————————————————————-
‘ コンストラクタ群
‘ —————————————————————-
”’
”’
Public Sub New()
MyBase.New(“業務処理においてエラーが発生しました。”)
Me.ErrorCode = “ERR_BIZ_DEFAULT”
Me.ErrorDetails = New Dictionary(Of String, String)()
End Sub
”’
”’
Public Sub New(message As String)
MyBase.New(message)
Me.ErrorCode = “ERR_BIZ_DEFAULT”
Me.ErrorDetails = New Dictionary(Of String, String)()
End Sub
”’
”’
Public Sub New(message As String, innerException As Exception)
MyBase.New(message, innerException)
Me.ErrorCode = “ERR_BIZ_DEFAULT”
Me.ErrorDetails = New Dictionary(Of String, String)()
End Sub
”’
”’
Public Sub New(errorCode As String, message As String)
MyBase.New(message)
Me.ErrorCode = errorCode
Me.ErrorDetails = New Dictionary(Of String, String)()
End Sub
”’
”’
Public Sub New(errorCode As String, message As String, errorDetails As Dictionary(Of String, String))
MyBase.New(message)
Me.ErrorCode = errorCode
Me.ErrorDetails = If(errorDetails, New Dictionary(Of String, String)())
End Sub
”’
”’
Public Sub New(errorCode As String, message As String, innerException As Exception, errorDetails As Dictionary(Of String, String))
MyBase.New(message, innerException)
Me.ErrorCode = errorCode
Me.ErrorDetails = If(errorDetails, New Dictionary(Of String, String)())
End Sub
‘ —————————————————————-
‘ シリアライズ対応(.NET Framework環境におけるRemotingやWCF対策)
‘ .NET Core / .NET 5+ では自動的に無視されますが、レガシー保守では必須の作法です。
‘ —————————————————————-
Protected Sub New(info As SerializationInfo, context As StreamingContext)
MyBase.New(info, context)
Me.ErrorCode = info.GetString(“ErrorCode”)
Me.ErrorDetails = CType(info.GetValue(“ErrorDetails”, GetType(Dictionary(Of String, String))), Dictionary(Of String, String))
End Sub
Public Overrides Sub GetObjectData(info As SerializationInfo, context As StreamingContext)
MyBase.GetObjectData(info, context)
info.AddValue(“ErrorCode”, Me.ErrorCode)
info.AddValue(“ErrorDetails”, Me.ErrorDetails)
End Sub
End Class
—
3. 実践:カスタム例外のスローと上位レイヤーでの多段階ハンドリング
この例外を実際の業務ロジック(例えば、銀行口座の出金処理)でどのようにスローし、プレゼンテーション層(UI層やWebAPIコントローラー)でどうキャッチすべきかを示す。
ビジネスロジック層(スロー側)
Public Class AccountService
Public Sub Withdraw(accountNumber As String, amount As Decimal)
‘ 残高照会(擬似コード)
Dim currentBalance As Decimal = GetBalance(accountNumber)
If currentBalance < amount Then ' 詳細なエラー情報を構築 Dim details As New Dictionary(Of String, String) From { {"AccountNumber", accountNumber}, {"RequestedAmount", amount.ToString("C")}, {"CurrentBalance", currentBalance.ToString("C")} } ' 業務例外をコード付きでスローする Throw New BusinessLogicException( errorCode:="ERR_ACC_INSUFFICIENT_FUNDS", message:="口座残高が不足しています。直近の取引履歴を確認してください。", errorDetails:=details ) End If ' 出金処理の継続... End Sub Private Function GetBalance(accountNumber As String) As Decimal ' DB等からの取得を想定 Return 5000D End Function End Class
プレゼンテーション層 / エントリポイント(キャッチ側)
上位レイヤーでは、`Exception` を雑に捕まえるのではなく、`BusinessLogicException` を明示的にキャッチし、その `ErrorCode` や `ErrorDetails` に応じてUIを制御する。
Public Sub ExecuteTransaction()
Dim service As New AccountService()
Try
service.Withdraw(“ACC-001”, 10000D)
Catch ex As BusinessLogicException
‘ 【極意】業務エラーのハンドリング(ユーザーへの適切なフィードバック)
Select Case ex.ErrorCode
Case “ERR_ACC_INSUFFICIENT_FUNDS”
‘ 追加情報から具体的な数値を引き出してログやUIに活用
Dim requested As String = If(ex.ErrorDetails.ContainsKey(“RequestedAmount”), ex.ErrorDetails(“RequestedAmount”), “不明”)
MessageBox.Show($”残高不足です。要求額: {requested}” & vbCrLf & ex.Message, “業務警告”, MessageBoxButtons.OK, MessageBoxIcon.Warning)
‘ 必要であればここで特定のリカバリ画面へ誘導するなどの処理
Case Else
‘ その他の業務エラー
MessageBox.Show($”予期せぬ業務エラーが発生しました。[Code: {ex.ErrorCode}] {ex.Message}”, “エラー”, MessageBoxButtons.OK, MessageBoxIcon.Error)
End Select
‘ 業務エラーは通常、致命的ではないためスタックトレースの大量出力は避け、監査ログ程度に留める
Catch ex As Exception
‘ 【極意】システムエラーのハンドリング(DB接続断、バグ等)
‘ ここに来るのは想定外の例外のみ。開発者向けに完全なスタックトレースをログ出力する。
System.Diagnostics.Trace.WriteLine($”[FATAL SYSTEM ERROR] {ex.ToString()}”)
MessageBox.Show(“システム内部で重大なエラーが発生しました。システム管理者にお問い合わせください。”, “致命的エラー”, MessageBoxButtons.OK, MessageBoxIcon.Stop)
‘ 必要に応じて再スロー、またはアプリの安全なシャットダウン
End Try
End Sub
—
4. チーフアーキテクトからの提言:例外設計の本質
例外機構とは、単なる「エラーメッセージの伝達手段」ではない。
「処理の継続が不可能になった文脈において、呼び出し元へ処理の選択肢(リカバリの権利)を型安全に返却するための契約(Contract)」である。
`BusinessLogicException` のようにエラーコードと詳細なディクショナリを持たせることで、以下のメリットが極限まで高まる。
- 国際化(i18n)への完全対応: メッセージのハードコーディングを排除し、UI側で `ErrorCode` をキーにしてリソースファイルから文言を引く設計へ容易に移行できる。
- 自動テストの容易性: 例外発生時の `ErrorCode` をアサートするだけで、業務ロジックの異常系テストが極めて堅牢になる。
- ログの構造化(Structured Logging): SerilogやNLogなどと組み合わせる際、`ErrorDetails` をそのままJSONとして吐き出すことで、ElasticsearchやKibana等を用いた高度なエラー分析基盤を構築できる。
VB.NETという言語の表現力を使いこなし、混沌としたレガシーコードから脱却せよ。優れたアーキテクチャとは、常に「意図がコードの型によって語られるもの」である。
