C#のthrowsは使えない?例外処理の書き方・throwとの違い・代替方法をわかりやすく解説

はじめに

C#で例外処理を調べていると、「C# throws」「C# throws 使い方」「C# throw throws 違い」といったキーワードにたどり着くことがあります。

結論からいうと、C#にはJavaのようなthrowsキーワードはありません

Javaでは、メソッドが投げる可能性のある例外をthrowsで宣言できますが、C#ではそのような書き方はできません。C#で例外を発生させるときは、throwsではなく**throw**を使います。

この記事では、C#でthrowsが使えない理由、Javaとの違い、throwの正しい使い方、try-catch-finallyによる例外処理、さらにthrowsの代わりに例外情報を伝える方法まで、初心者にもわかりやすく解説します。

1. C#のthrowsは使える?まず結論

1-1. C#にはJavaのようなthrowsキーワードは存在しない

C#には、Javaのようなthrowsキーワードは存在しません。

たとえば、Javaでは次のようにメソッド宣言にthrowsを書くことがあります。

Java
public void ReadFile() throws IOException {
// ファイル読み込み処理
}

しかし、C#で同じように書くことはできません。

C#
public void ReadFile() throws IOException
{
// C#ではコンパイルエラー
}

C#では、メソッドがどの例外を投げる可能性があるかを、メソッド宣言で強制的に書く仕組みはありません。

1-2. throwsを書こうとするとコンパイルエラーになる理由

C#でthrowsを書こうとすると、throwsがC#の予約語や有効な構文として認識されないため、コンパイルエラーになります。

C#のメソッド宣言は、基本的に次のような形です。

C#
public 戻り値の型 メソッド名(引数)
{
// 処理
}

例外を投げる可能性がある場合でも、メソッド宣言にはthrowsを書きません。

C#
public void ReadFile()
{
throw new IOException("ファイルの読み込みに失敗しました。");
}

つまり、C#では「このメソッドはこの例外を投げます」という情報を、言語構文として宣言するのではなく、コメントやドキュメント、設計によって伝えるのが一般的です。

1-3. C#で例外を投げるときはthrowを使う

C#で例外を発生させるには、throwを使います。

C#
throw new Exception("エラーが発生しました。");

たとえば、引数が不正な場合は次のように書けます。

C#
public void SetAge(int age)
{
if (age < 0)
{
throw new ArgumentOutOfRangeException(nameof(age), "年齢は0以上である必要があります。");
}
}

C#で覚えるべきキーワードは、throwsではなく**throw**です。

1-4. 「throws」と検索する人が混同しやすいポイント

「C# throws」と検索する人は、次のような点で混同していることが多いです。

Java経験者の場合、Javaのthrowsと同じ仕組みがC#にもあると思ってしまうことがあります。また、英語として「例外を投げる」を表すときに「method throws an exception」のように説明されるため、C#にもthrowsという構文があると誤解するケースもあります。

整理すると、次のようになります。

C#
// C#で例外を投げる
throw new Exception("エラー");

// C#では使えない
throws Exception

C#では、例外を「投げる」処理にはthrowを使い、メソッド宣言にthrowsを書くことはありません。

2. throwsとは何か?Javaとの違いを整理

2-1. Javaのthrowsはメソッドが投げる例外を宣言する仕組み

Javaのthrowsは、メソッドが呼び出し元に投げる可能性のある例外を宣言するための仕組みです。

Java
public void Load() throws IOException {
// IOExceptionを投げる可能性がある処理
}

このように書くことで、呼び出し元は「このメソッドはIOExceptionを投げる可能性がある」とわかります。

特にJavaにはチェック例外という仕組みがあり、特定の例外は呼び出し元でtry-catchするか、さらにthrowsで宣言しなければなりません。

2-2. C#にはチェック例外がない

C#には、Javaのようなチェック例外はありません。

チェック例外とは、コンパイラが例外処理を強制する例外のことです。Javaでは、メソッドがチェック例外を投げる可能性がある場合、呼び出し元はその例外を処理する必要があります。

一方、C#では例外を処理するかどうかは開発者の判断に任されています。

C#
public void Load()
{
throw new IOException("読み込みに失敗しました。");
}

このメソッドを呼び出す側は、必ずしもtry-catchを書く必要はありません。

C#
Load(); // コンパイルエラーにはならない

もちろん、必要であればtry-catchで処理できます。

C#
try
{
Load();
}
catch (IOException ex)
{
Console.WriteLine(ex.Message);
}

2-3. C#では例外宣言を強制しない設計になっている

C#では、メソッドが投げる例外を宣言することを言語として強制しません。

そのため、C#のメソッド宣言はJavaよりもシンプルに見えます。

C#
public void Save()
{
// 保存処理
}

ただし、例外宣言がないからといって、例外情報を伝えなくてよいわけではありません。

ライブラリやAPIを作る場合は、XMLコメントのexceptionタグを使って、発生する可能性のある例外をドキュメント化することが重要です。

C#
/// <summary>
/// ファイルを読み込みます。
/// </summary>
/// <exception cref="IOException">
/// ファイルの読み込みに失敗した場合に発生します。
/// </exception>
public void ReadFile()
{
throw new IOException("ファイルの読み込みに失敗しました。");
}

2-4. Java経験者がC#でつまずきやすい理由

Java経験者がC#でつまずきやすい理由は、例外処理の考え方が似ているようで違うからです。

C#にもtrycatchfinallythrowはあります。構文もJavaにかなり似ています。

しかし、C#には次の違いがあります。

Javaのようなthrows宣言はありません。チェック例外もありません。呼び出し元で例外処理を強制されません。

そのため、Javaの感覚でC#にthrowsを書こうとすると、コンパイルエラーになります。

C#では、例外の発生可能性をコード上で強制的に宣言するよりも、適切な例外型、XMLコメント、メソッド設計、ドキュメントによって伝える考え方が基本です。

3. C#のthrowの基本的な使い方

3-1. throwで新しい例外を発生させる書き方

C#で例外を発生させるには、throwを使います。

基本形は次のとおりです。

C#
throw new 例外型("例外メッセージ");

たとえば、単純に例外を投げる場合は次のように書きます。

C#
throw new Exception("エラーが発生しました。");

実際の開発では、Exceptionをそのまま使うよりも、状況に合った具体的な例外型を使うことが多いです。

3-2. throw new Exceptionの基本構文

throw new Exceptionは、C#で例外を投げる最も基本的な書き方です。

C#
public void Execute()
{
throw new Exception("処理に失敗しました。");
}

このコードが実行されると、その時点で通常の処理は中断され、例外が呼び出し元へ伝わります。

呼び出し元で例外を処理する場合は、try-catchを使います。

C#
try
{
Execute();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}

ただし、実務ではExceptionを直接投げると、何が原因のエラーなのか判断しにくくなります。そのため、できるだけ具体的な例外型を使うのが望ましいです。

3-3. ArgumentExceptionやInvalidOperationExceptionなど代表的な例外型

C#では、状況に応じた例外型を使うことで、エラーの意味を明確にできます。

引数が不正な場合はArgumentExceptionを使います。

C#
public void SetName(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("名前は必須です。", nameof(name));
}
}

引数がnullの場合はArgumentNullExceptionがよく使われます。

C#
public void Register(User user)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
}

引数の値が範囲外の場合はArgumentOutOfRangeExceptionが適しています。

C#
public void SetScore(int score)
{
if (score < 0 || score > 100)
{
throw new ArgumentOutOfRangeException(nameof(score), "スコアは0から100の範囲で指定してください。");
}
}

現在の状態では操作できない場合はInvalidOperationExceptionを使います。

C#
public void Start()
{
if (IsRunning)
{
throw new InvalidOperationException("すでに開始されています。");
}

IsRunning = true;
}

ファイルが見つからない場合はFileNotFoundException、権限がない場合はUnauthorizedAccessExceptionなど、標準で用意されている例外型を適切に使い分けることが大切です。

3-4. 例外メッセージを書くときの注意点

例外メッセージは、原因がわかるように具体的に書きます。

悪い例です。

C#
throw new Exception("エラー");

このメッセージでは、何が原因で、どのように対処すればよいのかわかりません。

改善例です。

C#
throw new InvalidOperationException("注文はすでに確定済みのため、変更できません。");

このように書くと、呼び出し元やログを確認する人が状況を理解しやすくなります。

ただし、例外メッセージにパスワード、アクセストークン、個人情報などの機密情報を含めてはいけません。例外メッセージはログに残ることが多いため、セキュリティ上のリスクになります。

3-5. throw式を使える場面

C#では、throwを式として使える場面があります。これをthrow式と呼びます。

たとえば、null合体演算子??と組み合わせて、値がnullの場合に例外を投げられます。

C#
public UserService(IRepository repository)
{
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
}

条件演算子でも使えます。

C#
var status = score >= 0
? "valid"
: throw new ArgumentOutOfRangeException(nameof(score));

throw式を使うとコードを簡潔に書けますが、使いすぎると読みにくくなることもあります。条件が複雑な場合は、通常のif文で書いたほうがわかりやすいです。

4. C#のtry-catch-finallyによる例外処理

4-1. try-catchの基本構文

C#で例外を処理するには、try-catchを使います。

C#
try
{
// 例外が発生する可能性のある処理
}
catch (Exception ex)
{
// 例外が発生したときの処理
}

例です。

C#
try
{
int number = int.Parse("abc");
}
catch (FormatException ex)
{
Console.WriteLine("数値に変換できませんでした。");
Console.WriteLine(ex.Message);
}

tryブロック内で例外が発生すると、対応するcatchブロックに処理が移ります。

4-2. 複数のcatchで例外の種類ごとに処理を分ける

C#では、複数のcatchを書くことで、例外の種類ごとに処理を分けられます。

C#
try
{
string text = File.ReadAllText("data.txt");
int number = int.Parse(text);
}
catch (FileNotFoundException ex)
{
Console.WriteLine("ファイルが見つかりません。");
}
catch (FormatException ex)
{
Console.WriteLine("ファイルの内容が数値ではありません。");
}
catch (Exception ex)
{
Console.WriteLine("予期しないエラーが発生しました。");
}

注意点として、catchは具体的な例外型から先に書きます。

Exceptionは多くの例外の基底クラスなので、最初に書いてしまうと、後ろの具体的なcatchに到達できなくなります。

C#
// よくない例
try
{
// 処理
}
catch (Exception ex)
{
// ここで多くの例外を捕まえてしまう
}
catch (FormatException ex)
{
// 到達できない
}

4-3. finallyで必ず実行したい後処理を書く

finallyは、例外が発生してもしなくても実行されるブロックです。

C#
try
{
Console.WriteLine("処理を開始します。");
}
catch (Exception ex)
{
Console.WriteLine("例外が発生しました。");
}
finally
{
Console.WriteLine("後処理を実行します。");
}

ファイル、ネットワーク接続、データベース接続など、後片付けが必要な処理に使われます。

ただし、C#ではIDisposableを実装したオブジェクトの解放には、using文やusing宣言を使うことが一般的です。

C#
using var stream = File.OpenRead("data.txt");
// streamはスコープを抜けると自動的に破棄される

finallyは便利ですが、リソース解放だけが目的であればusingを優先するとコードが読みやすくなります。

4-4. catchしないほうがよいケース

すべての例外を何でもcatchすればよいわけではありません。

たとえば、次のようなケースでは、無理にcatchしないほうがよい場合があります。

原因がわからず適切に対処できない例外、アプリケーションの継続が危険な例外、呼び出し元で処理すべき例外などです。

C#
try
{
ProcessOrder(order);
}
catch (Exception)
{
// 何もできないのに捕まえている
}

例外を捕まえたなら、ログを残す、ユーザーに通知する、別の例外に変換する、再スローするなど、意味のある処理を行う必要があります。

対処できない例外は、無理にその場で捕まえず、上位層に任せたほうがよいことがあります。

4-5. 例外を握りつぶすコードが危険な理由

例外を握りつぶすとは、catchした例外に対して何もせず、処理を続けてしまうことです。

C#
try
{
Save();
}
catch (Exception)
{
// 何もしない
}

このコードは危険です。

保存に失敗しているのに、呼び出し元は成功したと誤解するかもしれません。エラーの原因がログに残らないため、後から調査できなくなります。また、不正な状態のまま処理が続き、さらに大きな問題につながる可能性があります。

最低限、ログを残すなどの対応が必要です。

C#
try
{
Save();
}
catch (Exception ex)
{
logger.LogError(ex, "保存処理に失敗しました。");
throw;
}

例外を捕まえるときは、「その場で何をするのか」を明確にしておくことが大切です。

5. throw・throw ex・throwsの違い

5-1. throwとthrowsの違い

throwthrowsは似ていますが、意味も使える言語も異なります。

C#で使うのはthrowです。

C#
throw new InvalidOperationException("不正な操作です。");

throwは、例外を実際に発生させるためのキーワードです。

一方、throwsはJavaなどで使われる、メソッドが投げる可能性のある例外を宣言するためのキーワードです。

Java
public void Execute() throws IOException {
// Javaの例
}

C#ではthrowsは使えません。

5-2. throwとthrow exの違い

C#では、catchした例外を再度投げるときに、次の2つの書き方を見かけることがあります。

C#
throw;
C#
throw ex;

この2つは似ていますが、重要な違いがあります。

throw;は、現在捕まえている例外をそのまま再スローします。元のスタックトレースが保持されます。

一方、throw ex;は、例外を改めて投げ直す形になるため、スタックトレースがその行から始まったように扱われ、元の発生箇所が追いにくくなります。

5-3. 再スローではthrow;を使うべき理由

catchした例外をそのまま投げ直す場合は、throw;を使うべきです。

C#
try
{
Execute();
}
catch (Exception ex)
{
logger.LogError(ex, "処理に失敗しました。");
throw;
}

この書き方なら、例外が最初に発生した場所の情報を保ったまま、上位の呼び出し元へ例外を伝えられます。

ログを残したうえで上位層に処理を任せたい場合によく使います。

5-4. throw exでスタックトレースが失われる問題

throw ex;を使うと、例外のスタックトレースが再スローした場所から始まる形になり、元のエラー発生箇所がわかりにくくなります。

C#
try
{
Execute();
}
catch (Exception ex)
{
throw ex; // 推奨されない
}

このコードでは、実際にはExecute()の中で例外が発生していても、throw ex;の行が新しい起点のように見えてしまいます。

その結果、デバッグやログ調査が難しくなります。

再スローでは、基本的に次のように書きます。

C#
throw;

5-5. 違いがわかるサンプルコード

次のコードで違いを見てみましょう。

C#
public void MethodA()
{
MethodB();
}

public void MethodB()
{
throw new InvalidOperationException("MethodBで例外が発生しました。");
}

throw;で再スローする例です。

C#
try
{
MethodA();
}
catch (Exception ex)
{
Console.WriteLine("ログを出力します。");
throw;
}

この場合、スタックトレースにはMethodBで例外が発生した情報が残ります。

一方、throw ex;を使う例です。

C#
try
{
MethodA();
}
catch (Exception ex)
{
Console.WriteLine("ログを出力します。");
throw ex;
}

この場合、再スローした行が強調され、元の発生箇所を追いにくくなります。

つまり、次のように覚えるとよいです。

C#
throw new Exception(); // 新しく例外を投げる
throw; // catchした例外をそのまま再スローする
throw ex; // 原則使わない
throws // C#では使えない

6. C#でthrowsの代わりに使う方法

6-1. XMLコメントで発生する例外をドキュメント化する

C#にはthrowsがないため、メソッドが投げる可能性のある例外は、XMLコメントでドキュメント化するのが一般的です。

C#
/// <summary>
/// 指定されたIDのユーザーを取得します。
/// </summary>
/// <param name="id">ユーザーID。</param>
/// <returns>ユーザー情報。</returns>
/// <exception cref="ArgumentOutOfRangeException">
/// idが0以下の場合に発生します。
/// </exception>
/// <exception cref="UserNotFoundException">
/// 指定されたユーザーが存在しない場合に発生します。
/// </exception>
public User GetUser(int id)
{
if (id <= 0)
{
throw new ArgumentOutOfRangeException(nameof(id), "IDは1以上である必要があります。");
}

var user = FindUser(id);

if (user == null)
{
throw new UserNotFoundException(id);
}

return user;
}

XMLコメントを使うと、IDEの補完やドキュメント生成ツールで例外情報を確認しやすくなります。

6-2. exceptionタグで呼び出し元に例外情報を伝える

C#のXMLコメントでは、exceptionタグを使って例外情報を書けます。

C#
/// <exception cref="InvalidOperationException">
/// 注文がすでに確定済みの場合に発生します。
/// </exception>
public void ChangeOrder(Order order)
{
if (order.IsConfirmed)
{
throw new InvalidOperationException("確定済みの注文は変更できません。");
}
}

exceptionタグには、どの例外が、どの条件で発生するのかを書きます。

単に「エラー時に発生します」と書くよりも、具体的な条件を書くほうが親切です。

C#
/// <exception cref="ArgumentNullException">
/// orderがnullの場合に発生します。
/// </exception>

このように書くことで、呼び出し元はどのような入力や状態に注意すべきか理解しやすくなります。

6-3. メソッド名・戻り値・引数設計で失敗条件を明確にする

例外情報を伝える方法は、コメントだけではありません。メソッド名、戻り値、引数設計も重要です。

たとえば、失敗する可能性がある処理を次のように表現できます。

C#
public User GetUser(int id)

このメソッド名だと、ユーザーが存在しない場合に例外を投げるのか、nullを返すのかがわかりにくいです。

次のようにすると意図が明確になります。

C#
public User GetRequiredUser(int id)

「必ず取得する」という意味が強くなり、存在しない場合は例外を投げる設計だと伝わりやすくなります。

一方、存在しない可能性が通常のケースであれば、次のようにnullを返す設計も考えられます。

C#
public User? FindUser(int id)

メソッド名でGetFindTryなどを使い分けると、失敗時の扱いがわかりやすくなります。

6-4. Result型やTryパターンで例外以外の失敗表現を使う

通常の分岐として失敗が起こり得る場合は、例外ではなく戻り値で表現する方法もあります。

代表的なのがTryパターンです。

C#
public bool TryGetUser(int id, out User? user)
{
user = FindUser(id);
return user != null;
}

呼び出し側は次のように書けます。

C#
if (TryGetUser(1, out var user))
{
Console.WriteLine(user.Name);
}
else
{
Console.WriteLine("ユーザーが見つかりませんでした。");
}

int.TryParseもこのパターンの代表例です。

C#
if (int.TryParse("123", out var number))
{
Console.WriteLine(number);
}

また、プロジェクトによってはResult<T>のような型を定義して、成功・失敗を明示的に扱うこともあります。

C#
public class Result<T>
{
public bool IsSuccess { get; }
public T? Value { get; }
public string? ErrorMessage { get; }

private Result(bool isSuccess, T? value, string? errorMessage)
{
IsSuccess = isSuccess;
Value = value;
ErrorMessage = errorMessage;
}

public static Result<T> Success(T value)
=> new Result<T>(true, value, null);

public static Result<T> Failure(string errorMessage)
=> new Result<T>(false, default, errorMessage);
}

例外は強力ですが、通常の失敗まで何でも例外にするとコードが読みにくくなります。想定内の失敗は、TryパターンやResult型で表現するのも有効です。

6-5. 独自例外クラスを作成する

標準の例外型だけでは意味を表しにくい場合は、独自例外クラスを作成できます。

C#
public class UserNotFoundException : Exception
{
public int UserId { get; }

public UserNotFoundException(int userId)
: base($"ユーザーが見つかりません。UserId: {userId}")
{
UserId = userId;
}
}

使用例です。

C#
public User GetUser(int id)
{
var user = FindUser(id);

if (user == null)
{
throw new UserNotFoundException(id);
}

return user;
}

独自例外を使うと、呼び出し元で特定のエラーだけを捕まえやすくなります。

C#
try
{
var user = GetUser(10);
}
catch (UserNotFoundException ex)
{
Console.WriteLine(ex.Message);
}

ただし、何でも独自例外にすればよいわけではありません。ArgumentExceptionInvalidOperationExceptionなど標準の例外型で十分に表現できる場合は、標準の型を使うほうがシンプルです。

7. 実務でのC#例外処理のベストプラクティス

7-1. 想定外の異常には例外を使う

例外は、基本的に通常の処理ではない異常を表すために使います。

たとえば、次のようなケースです。

ファイルが存在する前提なのに存在しない、データベース接続に失敗した、不正な状態でメソッドが呼ばれた、外部APIから予期しないレスポンスが返った、といった場合です。

C#
if (!File.Exists(path))
{
throw new FileNotFoundException("必要なファイルが見つかりません。", path);
}

処理を続けると危険な場合や、呼び出し元に明確に異常を伝えたい場合は、例外を使うのが適しています。

7-2. 通常の分岐処理に例外を使いすぎない

一方で、通常の分岐としてよく起こることに例外を使いすぎるのは避けるべきです。

たとえば、ユーザー入力が数値ではないことは珍しくありません。この場合はint.Parseで例外を発生させるより、int.TryParseを使うほうが自然です。

C#
// 例外を使う例
try
{
int number = int.Parse(input);
}
catch (FormatException)
{
Console.WriteLine("数値を入力してください。");
}

より望ましい例です。

C#
if (int.TryParse(input, out var number))
{
Console.WriteLine(number);
}
else
{
Console.WriteLine("数値を入力してください。");
}

例外は通常の条件分岐の代わりではありません。失敗が想定内で頻繁に発生するなら、戻り値で表現する設計を検討します。

7-3. 具体的な例外型をcatchする

catch (Exception)ですべての例外を捕まえると、本来処理すべきでない例外まで捕まえてしまうことがあります。

C#
try
{
Process();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}

できるだけ具体的な例外型を指定しましょう。

C#
try
{
int number = int.Parse(input);
}
catch (FormatException ex)
{
Console.WriteLine("入力形式が不正です。");
}
catch (OverflowException ex)
{
Console.WriteLine("数値が大きすぎます。");
}

上位層でログを残す目的など、明確な理由がある場合はcatch (Exception)を使うこともあります。ただし、その場合でも握りつぶさず、必要に応じて再スローすることが重要です。

7-4. ログを残して必要に応じて再スローする

例外をcatchしたら、原因調査のためにログを残すことが重要です。

C#
try
{
service.Execute();
}
catch (Exception ex)
{
logger.LogError(ex, "サービス実行中にエラーが発生しました。");
throw;
}

このようにthrow;で再スローすれば、元のスタックトレースを保持したまま上位へ例外を伝えられます。

ただし、同じ例外を複数の層で何度もログ出力すると、ログが重複して読みにくくなります。どの層でログを取るかは、アプリケーション全体の設計として決めておくとよいです。

7-5. カスタム例外を使うべきケース

カスタム例外は、標準の例外型では業務的な意味を表しにくい場合に使います。

たとえば、次のようなケースです。

ユーザーが存在しない、在庫が不足している、注文がすでにキャンセルされている、権限が業務ルール上不足している、といったドメイン固有のエラーです。

C#
public class InsufficientStockException : Exception
{
public string ProductCode { get; }

public InsufficientStockException(string productCode)
: base($"在庫が不足しています。ProductCode: {productCode}")
{
ProductCode = productCode;
}
}

使用例です。

C#
if (stock < quantity)
{
throw new InsufficientStockException(productCode);
}

カスタム例外を使うことで、呼び出し元は業務エラーごとに処理を分けやすくなります。

C#
try
{
orderService.PlaceOrder(order);
}
catch (InsufficientStockException ex)
{
Console.WriteLine("在庫不足のため注文できません。");
}

ただし、標準例外で十分な場合まで独自例外を増やすと、管理が大変になります。意味のある単位で作ることが大切です。

7-6. async/awaitでの例外処理の注意点

C#のasync/awaitでも、例外処理は基本的にtry-catchで行います。

C#
try
{
await service.ExecuteAsync();
}
catch (HttpRequestException ex)
{
Console.WriteLine("HTTP通信に失敗しました。");
}

awaitされた非同期メソッド内で例外が発生すると、呼び出し元のcatchで捕まえられます。

C#
public async Task ExecuteAsync()
{
await Task.Delay(100);
throw new InvalidOperationException("非同期処理でエラーが発生しました。");
}

呼び出し側です。

C#
try
{
await ExecuteAsync();
}
catch (InvalidOperationException ex)
{
Console.WriteLine(ex.Message);
}

注意点は、非同期メソッドをawaitしないと、例外の扱いがわかりにくくなることです。

C#
// よくない例
ExecuteAsync();

このようにawaitせずに呼び出すと、例外を適切に捕捉できない可能性があります。

基本的には、非同期メソッドはawaitして、必要な場所でtry-catchします。

C#
try
{
await ExecuteAsync();
}
catch (Exception ex)
{
logger.LogError(ex, "非同期処理に失敗しました。");
}

8. C#のthrowsに関するよくある質問

8-1. C#でメソッドにthrowsを宣言できますか?

できません。

C#にはJavaのようなthrowsキーワードは存在しないため、次のようなコードは書けません。

C#
public void Execute() throws Exception
{
// コンパイルエラー
}

C#で例外を投げる場合は、メソッド内でthrowを使います。

C#
public void Execute()
{
throw new Exception("エラーが発生しました。");
}

発生する可能性のある例外を呼び出し元に伝えたい場合は、XMLコメントのexceptionタグなどでドキュメント化します。

8-2. C#にチェック例外はありますか?

C#にはJavaのようなチェック例外はありません。

つまり、あるメソッドが例外を投げる可能性があっても、呼び出し元で必ずtry-catchを書くことをコンパイラは強制しません。

C#
public void Load()
{
throw new IOException("読み込みに失敗しました。");
}

Load(); // try-catchしなくてもコンパイルは可能

ただし、コンパイルできるからといって例外処理を考えなくてよいわけではありません。必要な場所では適切にtry-catchし、ライブラリやAPIでは例外情報をドキュメント化することが大切です。

8-3. 呼び出し元に例外を伝えるにはどうすればよいですか?

C#では、主に次の方法で呼び出し元に例外情報を伝えます。

まず、適切な例外型を使います。

C#
throw new ArgumentNullException(nameof(user));

次に、XMLコメントで例外条件を明記します。

C#
/// <exception cref="ArgumentNullException">
/// userがnullの場合に発生します。
/// </exception>
public void Register(User user)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
}

また、メソッド名や戻り値の設計でも失敗条件を伝えられます。

C#
public bool TryGetUser(int id, out User? user)

例外で伝えるべき異常なのか、戻り値で表現すべき通常の失敗なのかを分けて考えることが重要です。

8-4. throw new Exceptionは使ってもよいですか?

文法的には使えます。

C#
throw new Exception("エラーが発生しました。");

ただし、実務ではExceptionを直接投げるよりも、具体的な例外型を使うほうが望ましいです。

たとえば、引数が不正ならArgumentException、引数がnullならArgumentNullException、現在の状態で操作できないならInvalidOperationExceptionを使います。

C#
throw new ArgumentException("名前は必須です。", nameof(name));

具体的な例外型を使うことで、呼び出し元が原因を判断しやすくなり、catchで処理を分けることもできます。

8-5. catchした例外をそのまま投げ直すにはどう書きますか?

catchした例外をそのまま投げ直す場合は、throw;を使います。

C#
try
{
Execute();
}
catch (Exception ex)
{
logger.LogError(ex, "処理に失敗しました。");
throw;
}

throw ex;は原則として避けます。

C#
try
{
Execute();
}
catch (Exception ex)
{
throw ex; // 推奨されない
}

throw;を使うことで、元のスタックトレースを保持したまま例外を再スローできます。ログを残して上位層に例外処理を任せたい場合は、throw;を使うのが基本です。

まとめ

C#には、Javaのようなthrowsキーワードはありません。

C#で例外を発生させるときは、throwsではなくthrowを使います。

C#
throw new InvalidOperationException("不正な操作です。");

Javaのthrowsは、メソッドが投げる可能性のある例外を宣言するための仕組みですが、C#にはチェック例外がないため、メソッド宣言で例外を強制的に書く必要はありません。

その代わり、C#では次の方法で例外を適切に扱います。

C#
/// <exception cref="InvalidOperationException">
/// 不正な状態で呼び出された場合に発生します。
/// </exception>

XMLコメントで例外をドキュメント化する、具体的な例外型を使う、通常の失敗にはTryパターンやResult型を使う、再スローではthrow;を使う、といった点が重要です。

特に覚えておきたい違いは次のとおりです。

C#
throw new Exception(); // 新しく例外を投げる
throw; // catchした例外をそのまま再スローする
throw ex; // スタックトレースの観点で非推奨
throws // C#では使えない

C#の例外処理では、何でもcatchするのではなく、必要な場所で、必要な例外だけを扱うことが大切です。

throwsが使えないことに戸惑うかもしれませんが、C#ではthrowtry-catch-finally、XMLコメント、Tryパターンなどを組み合わせることで、十分にわかりやすく安全な例外処理を設計できます。