はじめに
この記事では例外の発生箇所が特定できないパターンをサンプルコードを用いて紹介し、それに対する解決策をまとめます。
サンプルコード
以下の要件を満たすアプリケーションを考えます。
- ユーザのIDを指定するとユーザ名を出力する
- 例外が発生した場合、ユーザにはシステムエラーが起きた旨を伝える
- 例外の詳細はログに出力し、例外が発生した箇所を特定できるようにする
この要件を満たすように以下のように実装したとしましょう。
実装したコード
class Program
{
static void Main(string[] args)
{
var svc = new UserService();
try
{
var userId = int.Parse(args[0]);
var userName = svc.GetUserName(userId);
Console.WriteLine($"ユーザ名:{userName}");
}
catch (ServiceException ex)
{
// ユーザにはシステムエラーが発生した旨だけを伝える
Console.WriteLine("システムエラーが発生しました。");
// 例外の詳細はログに出力する
Logger.Write(ex.ToString());
}
}
}
public class UserService
{
public string GetUserName(int userId)
{
var dataAccessor = new UserDataAccessor();
try
{
var user = dataAccessor.Find(userId);
return user.Name;
}
catch (Exception ex)
{
throw new ServiceException($"ユーザ名の取得に失敗しました。 ユーザID:{userId}");
}
}
}
public class UserDataAccessor
{
public User Find(int userId)
{
// データベースのユーザテーブルから指定されたユーザを返す
// ユーザが見つからない場合はnullを返す
List<User> users = UsersTable.Load();
return users.FirstOrDefault(u => u.Id == userId);
}
}
実行結果
このコードを実行すると以下のような結果になりました。
ユーザが存在する場合
$ dotnet Sample.dll 1
ユーザ名:田中 太郎
ユーザが存在しない場合
$ dotnet Sample.dll 2
システムエラーが発生しました。
Sample.Service.ServiceException: ユーザ名の取得に失敗しました。 ユーザID:2
at Sample.Service.UserService.GetUserName(Int32 userId) in /Projects/Sample/Service/UserService.cs:line 19
at Sample.Program.Main(String[] args) in /Projects/Sample/Sample/Program.cs:line 14
例外の発生箇所を特定できない
出力結果を見ると、要件は満たせているように見えますが、実はこのコードは例外の発生箇所を特定できるようにするという要件を満たせていません。
ログの行番号から発生箇所を特定しようとすると、本来の例外の発生箇所ではない行に行き着いてしまいます。
at Sample.Service.UserService.GetUserName(Int32 userId) in /Projects/Sample/Service/UserService.cs:line 19
public class UserService
{
public string GetUserName(int userId)
{
var dataAccessor = new UserDataAccessor();
try
{
var user = dataAccessor.Find(userId); // 存在しないユーザの場合はuserがnullになる
return user.Name; // ここで例外が発生する。本来はここの行番号をログに書きたい
}
catch (Exception ex)
{
throw new ServiceException($"ユーザ名の取得に失敗しました。 ユーザID:{userId}"); // だが、ここの行番号がログに出力される
}
}
}
このように新しくnewした例外クラスをスローしてしまうと、本来の例外を上書きし、スタックトレースを消してしまうため、例外の発生箇所が特定できなくなってしまいます。
解決策
この問題を解決するには、例外クラスをnewする際に本来の例外をInnerException
プロパティに持たせるようにします。
自作した例外の場合は下記のように、InnerException
に入れておくためのコンストラクタを作成します。
public class UserService
{
public string GetUserName(int userId)
{
var dataAccessor = new UserDataAccessor();
try
{
var user = dataAccessor.Find(userId);
return user.Name;
}
catch(Exception ex)
{
// InnerExceptionに実際に起きた例外を入れてあげる
throw new ServiceException($"ユーザ名の取得に失敗しました。 ユーザID:{userId}", ex);
}
}
}
public class ServiceException : Exception
{
// 自作した例外の場合は発生した例外を InnerException に入れておくためのコンストラクタを作成する
public ServiceException(string message, Exception inner) : base(message, inner) { }
}
この状態でログを出力すると、下記のようなログを出力するようになり、ログの行番号から発生箇所を特定することができるようになります。
Sample.Service.ServiceException: ユーザ名の取得に失敗しました。 ユーザID:2
---> System.NullReferenceException: Object reference not set to an instance of an object.
at Sample.Service.UserService.GetUserName(Int32 userId) in /Projects/Sample/Service/UserService.cs:line 14
--- End of inner exception stack trace ---
at Sample.Service.UserService.GetUserName(Int32 userId) in /Projects/Sample/Service/UserService.cs:line 19
at Sample.Program.Main(String[] args) in /Projects/Sample/Sample/Program.cs:line 14
まとめ
例外の発生箇所を特定できないパターンとその解決策を紹介しました。
例外処理では本来の例外を消してしまうことがないように十分注意して処理するようにしましょう。
それではまた。
TomoProg