【入門編】Windows Formsにおけるカスタム例外クラスの設計と業務エラーハンドリング:ユーザー向けメッセージと開発者向けスタックトレースの分離戦略 – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク

こんにちは!VB.NETによるWindows Forms(WinForms)アプリケーション開発の世界へようこそ。

開発現場でこんな経験はありませんか?

  • 画面でエラーが起きたとき、ユーザーの画面に `System.NullReferenceException: オブジェクト参照がオブジェクト インスタンスに設定されていません。` という無機質な英数字の山(スタックトレース)がドカンと表示され、ユーザーがパニックになってしまう。
  • 逆に、エラーを隠そうとして `Catch ex As Exception` で何も表示しなかった結果、「ボタンを押しても何も起きないんだけど?」という問い合わせが殺到し、ログも残っていないため原因調査が泥沼化する……。

プログラミングの初学者や、VBAのマクロ記録からステップアップしてきた方が最初にぶつかる大きな壁、それが「エラーハンドリング(例外処理)の設計」です。

ここをクリアできれば、あなたの書くVB.NETコードは「ただ動くプログラム」から、「現場で信頼される堅牢なシステム」へと一気に進化しますよ!

今回は、業務システム開発の第一線で使われている「業務例外とシステム例外の分離」および「ユーザー向けメッセージと開発者向けログの分離戦略」を、分かりやすい図解と完全なサンプルコードで解説します。

—

1. エラーを2種類に分類しよう(業務例外 vs システム例外)

まず押さえておきたい本質は、「発生したエラー(例外)をすべて同じように扱ってはいけない」ということです。

アプリ内で起きる例外は、大きく次の2つに分けられます。

【発生した例外】
├─ ① 業務例外 (Business Exception)
│ └─ 例: 入力桁数オーバー、必須項目漏れ、在庫不足
│ └─ 対策: ユーザーに修正箇所を教えて再入力を促す(スタックトレース不要)
│
└─ ② システム例外 (System Exception)
└─ 例: DB接続切れ、ファイルアクセス権限エラー、バグ(NullReference等)
└─ 対策: ユーザーにはお詫びを表示し、開発者には詳細ログ(スタックトレース)を残す

なぜ分離が必要なのか?

| 区分 | 誰に対する情報か? | 画面に表示すべき内容 | 開発者が知りたい内容 |
| :— | :— | :— | :— |
| 業務例外 | 画面を操作しているユーザー | 「生年月日を入力してください」といった具体的で優しい誘導 | 発生した事実のみ(バグではないため緊急対応は不要なことが多い) |
| システム例外 | システムを保守する開発者 | 「システムエラーが発生しました。管理者にご連絡ください」というシンプルなお詫び | どこで落ちたかを示すスタックトレースと詳細な変数情報 |

ユーザーにスタックトレースを見せても解決できませんし、セキュリティ上も内部構造を暴露することになり危険です。一方、開発者に「エラーが発生しました」としか伝わらないログでは、原因究明が不可能です。

この両者のニーズを綺麗に満たす設計を構築していきましょう!

—

2. 独自の「業務例外クラス」を設計する

VB.NET標準の `Exception` クラスをそのまま投げると、それが「業務上の入力ミス」なのか「致命的なシステム障害」なのか判別できません。

そこで、業務エラーを表現するためのカスタム例外クラス(`BusinessException`)を自作します。

`BusinessException.vb` の実装

以下のコードをプロジェクトに追加してください。

Imports System

”’

”’ 業務ルール違反や入力不備など、ユーザーの再操作で解決可能な「業務例外」を表すクラスです。
”’

Public Class BusinessException
Inherits Exception

”’

”’ エラーコード(例: “ERR_QTY_001″ など、画面やマニュアルと紐付けるコード)
”’

Public Property ErrorCode As String

”’

”’ コンストラクタ:ユーザー向けメッセージを指定してインスタンス化します。
”’

”’ 画面に表示するわかりやすいメッセージ ”’ 任意の業務エラーコード Public Sub New(userMessage As String, Optional errorCode As String = “BIZ_ERR”)
‘ 親クラス(Exception)のMessageプロパティにユーザーメッセージを渡します
MyBase.New(userMessage)
Me.ErrorCode = errorCode
End Sub

”’

”’ 内部例外(InnerException)を保持するコンストラクタ
”’

Public Sub New(userMessage As String, innerException As Exception, Optional errorCode As String = “BIZ_ERR”)
MyBase.New(userMessage, innerException)
Me.ErrorCode = errorCode
End Sub
End Class

—

3. ユーザー用メッセージと開発者用ログを切り分ける「エラーハンドラー」

次に、発生した例外を解析し、「ユーザーに見せるダイアログの制御」と「開発者向けログの記録」を一手に引き受ける共通クラスを作成します。

`ErrorHandler.vb` の実装

Imports System.IO
Imports System.Windows.Forms

”’

”’ アプリケーション全体のエラー処理を一括管理する中央制御クラスです。
”’

Public Module ErrorHandler

”’

”’ 例外が発生した際に呼び出し、適切な画面表示とログ出力を行います。
”’

”’ 発生した例外オブジェクト Public Sub HandleException(ex As Exception)

‘ TYPE 1: 業務例外(BusinessException)の場合
If TypeOf ex Is BusinessException Then
Dim bizEx As BusinessException = DirectCast(ex, BusinessException)

‘ ユーザーには「入力の直し方」を優しく伝える(スタックトレースは見せない)
MessageBox.Show(
bizEx.Message,
$”入力・操作のご確認 ({bizEx.ErrorCode})”,
MessageBoxButtons.OK,
MessageBoxIcon.Warning
)

‘ ※業務例外はバグではないため、深刻なログとしては扱わない(必要に応じて操作ログへ)
Exit Sub
End If

‘ TYPE 2: システム例外(予期せぬエラー)の場合
‘ —————————————————-
‘ 1. 開発者向け:詳細情報(スタックトレース等)をログファイルに出力
LogErrorToFile(ex)

‘ 2. ユーザー向け:丁寧なお詫びメッセージのみを表示
Dim userMessage As String = “予期せぬシステムエラーが発生しました。” & vbCrLf &
“大変お手数ですが、管理者までご連絡ください。” & vbCrLf & vbCrLf &
“発生日時: ” & DateTime.Now.ToString(“yyyy/MM/dd HH:mm:ss”)

MessageBox.Show(
userMessage,
“システムエラー”,
MessageBoxButtons.OK,
MessageBoxIcon.Error
)
End Sub

”’

”’ システム例外の詳細情報(スタックトレース)をファイルに記録します。
”’

Private Sub LogErrorToFile(ex As Exception)
Try
‘ 実行ファイルと同じフォルダに “error.log” を作成(現場ではログライブラリを使用することも多いです)
Dim logPath As String = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, “error.log”)

Dim logContent As String = $”==========================================” & vbCrLf &
$”[発生日時] {DateTime.Now:yyyy/MM/dd HH:mm:ss}” & vbCrLf &
$”[例外型] {ex.GetType().FullName}” & vbCrLf &
$”[メッセージ] {ex.Message}” & vbCrLf &
$”[CallStack]” & vbCrLf &
$”{ex.StackTrace}” & vbCrLf &
$”==========================================” & vbCrLf & vbCrLf

‘ 追記モードで書き込み
File.AppendAllText(logPath, logContent)
Catch
‘ ログ書き込み自体が失敗した場合は、最悪アプリが落ちないように無視するかイベントログへ
End Try
End Sub

End Module

—

4. WinForms全体を包み込む「集約エラーハンドリング(すり抜け防止)」

各イベントハンドラー(ボタンのクリックなど)で `Try…Catch` を書き忘れてエラーが発生した場合、アプリが突然強制終了してしまいます。

これを防ぐために、Windows Formsが提供している「アプリケーション全体で捕獲するセーフティネット」を設置しましょう。

`ApplicationEvents.vb` を使う方法

Visual Studioの「プロジェクトのプロパティ」>「アプリケーション」タブにある「アプリケーション イベントの表示」ボタンを押すと生成される `ApplicationEvents.vb` に以下のように記述します。

Namespace My
Partial Friend Class MyApplication

”’

”’ UIスレッドでキャッチされなかった例外が発生したときに呼ばれます。
”’

Private Sub MyApplication_UnhandledException(sender As Object, e As Microsoft.VisualBasic.ApplicationServices.UnhandledExceptionEventArgs) Handles Me.UnhandledException

‘ 自作したエラーハンドラーに処理を委託
ErrorHandler.HandleException(e.Exception)

‘ e.ExitApplication = False にすると、アプリの強制終了を防ぎ継続させることができます
e.ExitApplication = False
End Sub

End Class
End Namespace

この「セーフティネット」があるおかげで、万が一コード内に `Try…Catch` の漏れがあっても、アプリが唐突に落ちることを防ぎ、確実にログを残せるようになります。

—

5. 実際の画面(Form)での使い方

それでは、実際の画面からこの仕組みをどう使うかを見てみましょう。

サンプルUIコード (`Form1.vb`)

Public Class Form1

”’

”’ 注文登録ボタンの処理例
”’

Private Sub btnSubmit_Click(sender As Object, e As EventArgs) Handles btnSubmit.Click
Try
‘ 業務ロジックの呼び出し
RegisterOrder(txtQuantity.Text)

MessageBox.Show(“注文を正常に受け付けました。”, “完了”, MessageBoxButtons.OK, MessageBoxIcon.Information)

Catch ex As Exception
‘ すべてのエラーを共通ハンドラーへパス!
‘ (BusinessException か通常の Exception かを共通ハンドラーが自動判断してくれます)
ErrorHandler.HandleException(ex)
End Try
End Sub

”’

”’ 業務ロジック(本来は別クラスに分けるのが望ましいです)
”’

Private Sub RegisterOrder(quantityText As String)
‘ — 1. バリデーション(業務チェック) —
Dim quantity As Integer
If Not Integer.TryParse(quantityText, quantity) Then
‘ 業務例外を発行(ユーザーへのアドバイスを含める)
Throw New BusinessException(“数量には半角数字を入力してください。”, “ERR_INPUT_001”)
End If

If quantity <= 0 Then ' 業務例外を発行 Throw New BusinessException("数量には1以上の数値を指定してください。", "ERR_INPUT_002") End If ' --- 2. 業務処理(システム処理) --- ' 例として、故意にゼロ除算(システム例外)を起こしてみます Dim dummyCalc As Integer = 100 \ (quantity - 5) ' quantityが5のとき DivideByZeroException が発生! End Sub End Class

動作結果の比較

1. 数量に「あ」と入力した場合(業務例外)

  • 画面表示: 黄色の警告アイコンで「数量には半角数字を入力してください。(ERR_INPUT_001)」と親切に表示されます。
  • ログファイル: 出力されません(バグではないため)。

2. 数量に「5」と入力した場合(システム例外:ゼロ除算発生)

  • 画面表示: 赤いエラーアイコンで「予期せぬシステムエラーが発生しました。管理者にご連絡ください。」とお詫びが表示されます。
  • ログファイル(`error.log`): どの行でゼロ除算が起きたのか、スタックトレースが綺麗に記録されます!

—

6. 先輩エンジニアからのワンポイント・アドバイス

① 例外を「制御フロー(If文の代わり)」に使わないこと!

例外(`Throw`)を投げると、.NETランタイムは現在の実行スタック情報を収集するため、通常の処理分岐(`If` 文)に比べて大きなCPUコストがかかります。
「入力チェックでエラーになる確率が高い」と分かっている場所は、できる限り事前に `If` 文で判定し、どうしても処理を中断して上位に通知したい場合のみ `BusinessException` を投げましょう。

② `Throw ex` ではなく `Throw` と書く!

Catch節で捕まえた例外をそのまま上位に再スロー(投げ直し)したい場合、絶対に `Throw ex` と書いてはいけません。

Try
‘ 何らかの処理
Catch ex As Exception
‘ ✕ NG: これをやると、ここまでのスタックトレース(呼び出し履歴)がリセットされて消えてしまいます!
Throw ex

‘ ◯ OK: 元のスタックトレースを保持したまま上位へ投げ直せます
Throw
End Try

—

まとめ:マスターへの道

今回学んだ「エラーハンドリング分離戦略」のポイントをおさらいしましょう。

1. 例外を「業務例外」と「システム例外」に明確に分ける。
2. `BusinessException` クラスを自作し、ユーザー向けメッセージを乗せる。
3. `ErrorHandler` モジュールを作り、画面表示とファイルログ出力を一括管理する。
4. `ApplicationEvents` で、すり抜けた例外のセーフティネットを張る。

このパターンは、Windows Formsだけでなく、WPFやASP.NETといった他のプラットフォーム、さらにはC#など他の言語でもそのまま応用できる万能なアーキテクチャです。

一見難しそうに見えますが、一度自分のテンプレートを作ってしまえば、今後の開発が劇的にラクになり、アプリの品質も跳ね上がります。

ここをクリアできれば、Visual Basic (VB / VB.NET)の基本はバッチリですよ!自信を持って、さらに高度な設計にチャレンジしていきましょう!

タイトルとURLをコピーしました