C#のfinallyとは?実行タイミング・使い方・returnや例外時の挙動を初心者向けに解説

はじめに

C#で例外処理を学んでいると、trycatchとあわせてfinallyというキーワードが出てきます。

finallyは、例外が発生したかどうかに関係なく、最後に実行したい処理を書くための仕組みです。たとえば、ファイルを閉じる、データベース接続を解放する、ロックを解除するなど、「処理が成功しても失敗しても必ず行いたい後片付け」に使われます。

この記事では、C#のfinallyについて、基本的な書き方、実行タイミング、returnがある場合の挙動、例外発生時の注意点、using文との違いまで、初心者向けにわかりやすく解説します。

1. C#のfinallyとは?

1-1. finallyは例外の有無に関係なく最後に実行される処理

C#のfinallyは、tryブロックの後に書くことができるブロックです。

基本的には、try内の処理が正常に終わった場合でも、例外が発生した場合でも、最後に実行されます。

C#
try
{
Console.WriteLine("処理を開始します");
}
finally
{
Console.WriteLine("最後に実行されます");
}

このコードでは、tryの中で例外が発生していなくても、finallyの中の処理が実行されます。

finallyは、「この処理だけは最後に必ず行いたい」という場面で使うものだと考えると理解しやすいです。

1-2. try・catch・finallyの役割の違い

C#の例外処理では、主にtrycatchfinallyを使います。それぞれの役割は次のとおりです。

tryは、例外が発生する可能性のある処理を書く場所です。

C#
try
{
int result = 10 / 0;
}

catchは、try内で発生した例外を受け取り、エラー処理を行う場所です。

C#
catch (DivideByZeroException ex)
{
Console.WriteLine("0で割ることはできません");
}

finallyは、例外の有無に関係なく、最後に実行したい処理を書く場所です。

C#
finally
{
Console.WriteLine("後処理を行います");
}

まとめると、tryは「通常処理」、catchは「例外が起きたときの処理」、finallyは「最後に行う後処理」です。

1-3. finallyを使う主な目的はリソース解放や後処理

finallyを使う主な目的は、リソースの解放や後処理です。

たとえば、ファイルを開いた場合、処理が成功しても失敗しても、最後にはファイルを閉じる必要があります。データベース接続を開いた場合も、使い終わったら接続を閉じる必要があります。

例外が発生したからといって後処理が実行されないと、ファイルが開きっぱなしになったり、データベース接続が残ったりする可能性があります。

そのような問題を防ぐために、finallyを使って確実に後処理を行います。

2. finallyの基本的な書き方

2-1. try-finallyの構文

finallyは、catchがなくても使えます。

C#
try
{
// 例外が発生する可能性のある処理
}
finally
{
// 最後に必ず実行したい処理
}

この形をtry-finallyと呼びます。

tryの中で例外が発生しなければ、tryの処理が終わったあとにfinallyが実行されます。

tryの中で例外が発生した場合も、finallyは実行されます。ただし、catchがないため、その例外は呼び出し元に伝わります。

2-2. try-catch-finallyの構文

例外を処理したうえで、最後に後処理も行いたい場合は、try-catch-finallyを使います。

C#
try
{
// 例外が発生する可能性のある処理
}
catch (Exception ex)
{
// 例外が発生したときの処理
}
finally
{
// 例外の有無に関係なく最後に実行する処理
}

この場合、処理の流れは次のようになります。

まずtryが実行されます。例外が発生した場合はcatchが実行されます。そして最後にfinallyが実行されます。

例外が発生しなかった場合は、catchは実行されず、tryのあとにfinallyが実行されます。

2-3. finally内に書く処理の具体例

finally内には、主に次のような処理を書きます。

ファイルやストリームを閉じる処理、データベース接続を閉じる処理、ロックを解除する処理、一時的に変更した状態を元に戻す処理、ログ出力などです。

たとえば、次のような後処理が考えられます。

C#
finally
{
Console.WriteLine("処理を終了します");
}

より実用的には、次のようにリソース解放を行います。

C#
finally
{
if (reader != null)
{
reader.Close();
}
}

ただし、現在のC#では、ファイルやストリームなどIDisposableを実装しているオブジェクトには、using文を使うことも多いです。

2-4. 初心者向けのシンプルなサンプルコード

次のコードで、finallyの基本的な動きを確認してみましょう。

C#
using System;

class Program
{
static void Main()
{
try
{
Console.WriteLine("try開始");
int number = 10 / 2;
Console.WriteLine($"計算結果: {number}");
}
catch (Exception ex)
{
Console.WriteLine($"例外が発生しました: {ex.Message}");
}
finally
{
Console.WriteLine("finallyが実行されました");
}

Console.WriteLine("プログラム終了");
}
}

実行結果は次のようになります。

try開始
計算結果: 5
finallyが実行されました
プログラム終了

この例では例外が発生していないため、catchは実行されません。しかし、finallyは実行されています。

次に、例外が発生するように変更してみます。

C#
int number = 10 / 0;

この場合、catchが実行されたあとにfinallyが実行されます。

try開始
例外が発生しました: Attempted to divide by zero.
finallyが実行されました
プログラム終了

このように、finallyは正常終了時にも例外発生時にも実行されます。

3. finallyが実行されるタイミング

3-1. tryブロックが正常終了した場合

tryブロックが正常に終了した場合、tryの処理がすべて終わったあとにfinallyが実行されます。

C#
try
{
Console.WriteLine("tryの処理");
}
finally
{
Console.WriteLine("finallyの処理");
}

実行結果は次のようになります。

tryの処理
finallyの処理

例外が発生していなくても、finallyは実行されます。

3-2. catchブロックで例外を処理した場合

try内で例外が発生し、その例外がcatchで処理された場合、catchの処理が終わったあとにfinallyが実行されます。

C#
try
{
Console.WriteLine("tryの処理");
int result = 10 / 0;
}
catch (DivideByZeroException)
{
Console.WriteLine("catchの処理");
}
finally
{
Console.WriteLine("finallyの処理");
}

実行結果は次のようになります。

tryの処理
catchの処理
finallyの処理

この場合、処理の順番はtrycatchfinallyです。

3-3. catchしない例外が発生した場合

try内で例外が発生し、その例外を処理するcatchがない場合でも、finallyは実行されます。

C#
static void Main()
{
try
{
Console.WriteLine("tryの処理");
int result = 10 / 0;
}
finally
{
Console.WriteLine("finallyの処理");
}
}

このコードでは、DivideByZeroExceptionが発生します。catchがないため例外は処理されませんが、finallyは実行されます。

tryの処理
finallyの処理

その後、例外は呼び出し元に伝わります。Mainメソッド内で処理されなければ、プログラムは例外によって終了します。

3-4. メソッドを抜ける直前にfinallyが実行される流れ

finallyは、メソッドを抜ける直前にも実行されます。

たとえば、try内でreturnが実行される場合でも、すぐにメソッドを抜けるのではなく、その前にfinallyが実行されます。

C#
static int GetNumber()
{
try
{
Console.WriteLine("try");
return 1;
}
finally
{
Console.WriteLine("finally");
}
}

このメソッドを呼び出すと、次の順番で処理されます。

try
finally

returnで戻り値を返す直前に、finallyが実行されるという点が重要です。

4. returnがある場合のfinallyの挙動

4-1. try内でreturnしてもfinallyは実行される

try内にreturnがある場合でも、finallyは実行されます。

C#
static int Sample()
{
try
{
Console.WriteLine("try");
return 100;
}
finally
{
Console.WriteLine("finally");
}
}

このコードでは、try内でreturn 100;が実行されます。しかし、メソッドを抜ける前にfinallyが実行されます。

実行順序は次のとおりです。

try
finally

そして、最終的に100が戻り値として返されます。

4-2. catch内でreturnしてもfinallyは実行される

catch内にreturnがある場合でも、finallyは実行されます。

C#
static int Sample()
{
try
{
int result = 10 / 0;
return result;
}
catch
{
Console.WriteLine("catch");
return -1;
}
finally
{
Console.WriteLine("finally");
}
}

実行結果は次のようになります。

catch
finally

この場合、catch内でreturn -1;が実行されますが、その前にfinallyが割り込むように実行されます。最終的には-1が戻り値として返されます。

4-3. finally内でreturnを書くべきでない理由

C#では、finallyブロックの中にreturnを書くことはできません。

たとえば、次のようなコードはコンパイルエラーになります。

C#
static int Sample()
{
try
{
return 1;
}
finally
{
return 2; // コンパイルエラー
}
}

finallyは後処理を書くための場所です。戻り値を決める場所ではありません。

また、仮に他の言語でfinally内にreturnを書ける場合でも、元の戻り値や例外の流れを上書きしてしまい、コードの動きがわかりにくくなります。

C#では、finally内にreturnを書かず、戻り値はtrycatchの外で明確に扱うのが基本です。

4-4. return値とfinallyの実行順序をコードで確認

次のコードで、returnfinallyの実行順序を確認してみましょう。

C#
using System;

class Program
{
static void Main()
{
int result = GetValue();
Console.WriteLine($"戻り値: {result}");
}

static int GetValue()
{
try
{
Console.WriteLine("try内でreturnします");
return 10;
}
finally
{
Console.WriteLine("finallyが実行されます");
}
}
}

実行結果は次のようになります。

try内でreturnします
finallyが実行されます
戻り値: 10

この結果から、try内でreturnしても、戻り値が呼び出し元に返る前にfinallyが実行されることがわかります。

5. 例外発生時のfinallyの挙動

5-1. 例外が発生してもfinallyは実行される

finallyの重要な特徴は、例外が発生しても実行されることです。

C#
try
{
Console.WriteLine("try開始");
throw new Exception("エラーが発生しました");
}
catch (Exception ex)
{
Console.WriteLine($"catch: {ex.Message}");
}
finally
{
Console.WriteLine("finally");
}

実行結果は次のようになります。

try開始
catch: エラーが発生しました
finally

例外が発生しても、catchで処理されたあとにfinallyが実行されています。

5-2. catchがない場合でもfinallyは実行される

catchがない場合でも、finallyは実行されます。

C#
try
{
Console.WriteLine("try開始");
throw new Exception("エラー");
}
finally
{
Console.WriteLine("finally");
}

この場合、例外はこの場所では処理されません。しかし、例外が呼び出し元に伝わる前にfinallyが実行されます。

つまり、finallyは「例外を処理するため」ではなく、「後処理を確実に行うため」のものです。

5-3. finally内で例外が発生した場合の注意点

finally内でも例外は発生する可能性があります。

C#
try
{
Console.WriteLine("try");
}
finally
{
Console.WriteLine("finally");
throw new Exception("finally内の例外");
}

このようなコードでは、finally内で新しい例外が発生します。

finallyは後処理を書く場所ですが、後処理の中で例外が発生すると、プログラムの流れがさらに複雑になります。そのため、finally内ではできるだけ例外が発生しにくい処理を書くべきです。

必要であれば、finally内の処理にも個別に例外対策を行います。

C#
finally
{
try
{
// 後処理
}
catch (Exception ex)
{
Console.WriteLine($"後処理中に例外が発生しました: {ex.Message}");
}
}

ただし、何でも握りつぶせばよいわけではありません。ログを残すなど、問題を追跡できる形にしておくことが大切です。

5-4. 元の例外が隠れてしまうケース

try内で例外が発生し、さらにfinally内でも例外が発生すると、元の例外がわかりにくくなることがあります。

C#
try
{
throw new Exception("try内の例外");
}
finally
{
throw new Exception("finally内の例外");
}

この場合、try内で発生した例外よりも、finally内で発生した例外が表に出ることがあります。

結果として、本当に調査すべき原因である「try内の例外」が隠れてしまう可能性があります。

そのため、finally内では新しい例外を発生させないように注意しましょう。特に、ファイルを閉じる処理、接続を解放する処理、ログ出力などでも例外が発生する可能性があるため、実装時には慎重に扱う必要があります。

6. finallyが実行されないケース

6-1. アプリケーションやプロセスが強制終了した場合

finallyは多くの場合に実行されますが、どんな状況でも必ず実行されるわけではありません。

たとえば、アプリケーションやプロセスが外部から強制終了された場合、finallyが実行されないことがあります。

OSのタスクマネージャーからプロセスを終了した場合や、コンテナ環境でプロセスが強制停止された場合などです。

このような状況では、C#の通常の制御フローを通らないため、finallyの実行が保証されません。

6-2. Environment.FailFastを呼び出した場合

Environment.FailFastを呼び出すと、アプリケーションは即座に終了します。

C#
try
{
Environment.FailFast("致命的なエラー");
}
finally
{
Console.WriteLine("finally");
}

このような場合、通常の例外処理とは異なり、finallyが実行されないことがあります。

Environment.FailFastは、回復不能な致命的エラーが発生したときに、アプリケーションを即時終了するための仕組みです。そのため、通常の後処理を期待して使うものではありません。

6-3. StackOverflowExceptionなど一部の致命的な例外

StackOverflowExceptionのような致命的な例外では、finallyが通常どおり実行されない場合があります。

たとえば、無限再帰によってスタック領域を使い切った場合、プログラムは安全に後処理を続けられない状態になります。

C#
static void Recursive()
{
Recursive();
}

このような状態では、通常の例外処理やfinallyの実行が期待どおりに行われないことがあります。

finallyは便利ですが、アプリケーションが正常に制御を続けられる状況で有効な仕組みだと理解しておきましょう。

6-4. PCや実行環境が停止した場合

PCの電源が落ちた場合、OSがクラッシュした場合、実行環境そのものが停止した場合も、finallyは実行されません。

たとえば、停電、強制シャットダウン、ハードウェア障害などです。

このようなケースでは、プログラム側で制御できないため、finallyによる後処理は保証されません。

重要なデータを扱う場合は、finallyだけに頼るのではなく、こまめな保存、トランザクション、ログ、バックアップなども組み合わせることが大切です。

7. finallyの実用的な使い方

7-1. ファイルやストリームを閉じる

finallyの代表的な使い方は、ファイルやストリームを閉じる処理です。

C#
StreamReader? reader = null;

try
{
reader = new StreamReader("sample.txt");
string text = reader.ReadToEnd();
Console.WriteLine(text);
}
catch (IOException ex)
{
Console.WriteLine($"ファイル処理でエラーが発生しました: {ex.Message}");
}
finally
{
if (reader != null)
{
reader.Close();
}
}

このコードでは、ファイルの読み込み中に例外が発生しても、最後にreader.Close()が呼ばれます。

ただし、現在は次のようにusing文を使う書き方が一般的です。

C#
using StreamReader reader = new StreamReader("sample.txt");
string text = reader.ReadToEnd();
Console.WriteLine(text);

using文を使うと、内部的にfinallyと同じような後処理が行われます。

7-2. データベース接続を解放する

データベース接続も、使い終わったら必ず解放する必要があります。

C#
SqlConnection? connection = null;

try
{
connection = new SqlConnection(connectionString);
connection.Open();

// データベース処理
}
catch (SqlException ex)
{
Console.WriteLine($"データベースエラー: {ex.Message}");
}
finally
{
if (connection != null)
{
connection.Close();
}
}

データベース接続を閉じ忘れると、接続数の上限に達したり、パフォーマンスに悪影響が出たりする可能性があります。

このようなリソース管理にfinallyは役立ちます。

ただし、SqlConnectionIDisposableを実装しているため、通常はusing文を使うほうが簡潔です。

C#
using SqlConnection connection = new SqlConnection(connectionString);
connection.Open();

// データベース処理

7-3. ロックや一時的な状態を解除する

finallyは、ロックを解除する処理にも使われます。

C#
object lockObject = new object();
bool lockTaken = false;

try
{
Monitor.Enter(lockObject, ref lockTaken);

// 排他制御が必要な処理
}
finally
{
if (lockTaken)
{
Monitor.Exit(lockObject);
}
}

ロックを取得したあとに例外が発生すると、ロックが解除されないままになる可能性があります。すると、他の処理が待ち続けてしまい、デッドロックの原因になることがあります。

そのため、ロックを取得したら、finallyで確実に解除することが重要です。

なお、通常のlock文を使う場合は、C#が内部的に適切な後処理を行ってくれます。

C#
lock (lockObject)
{
// 排他制御が必要な処理
}

7-4. ログ出力や後処理を行う

finallyは、処理の終了ログを出力する場面でも使えます。

C#
try
{
Console.WriteLine("処理を開始しました");

// メイン処理
}
catch (Exception ex)
{
Console.WriteLine($"エラー: {ex.Message}");
}
finally
{
Console.WriteLine("処理を終了しました");
}

このように書くことで、処理が成功した場合でも失敗した場合でも、終了ログを残せます。

ただし、ログ出力自体が失敗する可能性もあります。特にファイルや外部サービスにログを送る場合は、finally内でさらに例外が発生しないよう注意しましょう。

8. finallyとusing文・Disposeの違い

8-1. using文はfinallyを簡潔に書くための仕組み

C#のusing文は、リソース解放を簡潔に書くための仕組みです。

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

C#
using StreamReader reader = new StreamReader("sample.txt");
string text = reader.ReadToEnd();
Console.WriteLine(text);

このコードでは、処理が終わると自動的にreader.Dispose()が呼ばれます。

using文は、内部的にはtry-finallyに近い形で動作します。つまり、例外が発生してもリソースが解放されるようになっています。

finallyを手動で書くよりも、using文を使ったほうが短く、安全で、読みやすいコードになります。

8-2. IDisposableとDisposeの基本

using文で扱えるのは、基本的にIDisposableを実装しているオブジェクトです。

IDisposableは、不要になったリソースを解放するためのDisposeメソッドを持つインターフェイスです。

C#
public interface IDisposable
{
void Dispose();
}

ファイル、ストリーム、データベース接続などは、使い終わったら明示的に解放する必要があります。そのため、多くの場合IDisposableを実装しています。

using文を使うと、スコープを抜けるときに自動でDisposeが呼ばれます。

C#
using var resource = new SomeDisposableResource();

// resourceを使う処理

このスコープを抜けるときに、resource.Dispose()が呼ばれます。

8-3. using文を使うべきケース

ファイル、ストリーム、データベース接続、HTTP関連の一部リソースなど、IDisposableを実装しているオブジェクトを扱う場合は、基本的にusing文を使うのがおすすめです。

C#
using FileStream stream = new FileStream("sample.txt", FileMode.Open);
using StreamReader reader = new StreamReader(stream);

string text = reader.ReadToEnd();

このように書けば、処理が終わったときに自動でリソースが解放されます。

finallyを手動で書くよりも、コードが短くなり、解放忘れも防ぎやすくなります。

特に初心者の場合、リソース解放にはまずusing文を使うと覚えておくとよいでしょう。

8-4. finallyを使ったほうがよいケース

一方で、すべての後処理をusing文で書けるわけではありません。

finallyを使ったほうがよいケースもあります。

たとえば、IDisposableではない独自の後処理を行いたい場合です。

C#
bool isProcessing = false;

try
{
isProcessing = true;

// 何らかの処理
}
finally
{
isProcessing = false;
}

このように、一時的に変更したフラグを元に戻す処理は、using文よりもfinallyのほうが自然です。

また、複数の後処理をまとめて制御したい場合や、特定の状態を必ず復元したい場合にもfinallyが使えます。

つまり、IDisposableのリソース解放にはusing文、それ以外の後処理にはfinallyを使う、という考え方が基本です。

9. finallyを使うときの注意点

9-1. finally内に重い処理を書きすぎない

finallyは最後に実行される重要な処理ですが、何でも書いてよいわけではありません。

finally内に重い処理を書きすぎると、メソッドの終了や例外の伝播が遅くなります。

たとえば、大量のデータ処理、時間のかかる通信処理、複雑な計算などをfinallyに書くと、コードの見通しが悪くなります。

finallyには、必要最小限の後処理を書くのが基本です。

9-2. finally内で例外を発生させない

finally内で例外が発生すると、元の例外や本来の処理結果が隠れてしまうことがあります。

C#
try
{
throw new Exception("元の例外");
}
finally
{
throw new Exception("finally内の例外");
}

このようなコードは、原因調査を難しくします。

finally内では、できるだけ例外が発生しない処理を書くべきです。どうしても例外が発生する可能性がある場合は、ログを残す、個別に例外処理を行うなどの対策を考えましょう。

9-3. returnやthrowをfinally内に書かない

C#ではfinally内にreturnを書くことはできません。

また、finally内でthrowを書くことも、基本的には避けるべきです。

C#
finally
{
throw new Exception("finallyで例外");
}

このようにすると、元の例外や処理の流れがわかりにくくなります。

finallyは、戻り値を決めたり、新しい例外を発生させたりする場所ではありません。あくまで後処理を行う場所として使いましょう。

9-4. 例外処理と後処理の責務を分ける

catchfinallyには、それぞれ役割があります。

catchは例外を処理する場所です。エラーメッセージを出す、代替処理を行う、例外を再スローするなどの処理を書きます。

一方、finallyは後処理を行う場所です。リソース解放、状態の復元、終了ログなどを書きます。

この2つの責務を混ぜると、コードが読みにくくなります。

C#
try
{
// メイン処理
}
catch (Exception ex)
{
// 例外処理
}
finally
{
// 後処理
}

このように役割を分けることで、保守しやすいコードになります。

10. finallyに関するよくある疑問

10-1. finallyはcatchがなくても使える?

はい、finallycatchがなくても使えます。

C#
try
{
// 処理
}
finally
{
// 後処理
}

この形は、例外をその場で処理する必要はないが、後処理だけは必ず行いたい場合に使います。

例外が発生した場合、finallyが実行されたあと、その例外は呼び出し元に伝わります。

10-2. finallyは必ず実行される?

通常の制御フローでは、finallyは基本的に実行されます。

正常終了した場合、例外が発生してcatchで処理された場合、例外が処理されず呼び出し元に伝わる場合、returnでメソッドを抜ける場合でも、finallyは実行されます。

ただし、プロセスの強制終了、Environment.FailFast、実行環境の停止、致命的な例外などの場合は、finallyが実行されないことがあります。

そのため、「通常は実行されるが、絶対にどんな状況でも実行されるわけではない」と理解しておきましょう。

10-3. finallyとcatchはどちらが先に実行される?

例外が発生し、その例外を処理できるcatchがある場合は、catchが先に実行され、その後にfinallyが実行されます。

C#
try
{
throw new Exception();
}
catch
{
Console.WriteLine("catch");
}
finally
{
Console.WriteLine("finally");
}

実行結果は次のようになります。

catch
finally

つまり、順番はtrycatchfinallyです。

例外が発生しなかった場合は、catchは実行されず、tryのあとにfinallyが実行されます。

10-4. async・awaitでもfinallyは使える?

はい、asyncawaitを使う非同期メソッドでもfinallyは使えます。

C#
static async Task SampleAsync()
{
try
{
Console.WriteLine("非同期処理開始");
await Task.Delay(1000);
Console.WriteLine("非同期処理完了");
}
finally
{
Console.WriteLine("finally");
}
}

この場合も、try内の非同期処理が正常に終わった場合や、例外が発生した場合に、finallyが実行されます。

また、finally内でawaitを使うこともできます。

C#
static async Task SampleAsync()
{
try
{
await Task.Delay(1000);
}
finally
{
await Task.Delay(500);
Console.WriteLine("非同期の後処理");
}
}

非同期処理でも、後処理を確実に行いたい場面ではfinallyが役立ちます。

ただし、非同期の後処理が長くなりすぎると、メソッドの完了が遅くなるため注意しましょう。

まとめ

C#のfinallyは、例外の有無に関係なく、最後に実行したい後処理を書くための仕組みです。

try内の処理が正常に終わった場合でも、例外が発生してcatchで処理された場合でも、finallyは実行されます。また、trycatchの中でreturnした場合でも、メソッドを抜ける直前にfinallyが実行されます。

一方で、プロセスの強制終了、Environment.FailFast、実行環境の停止、致命的な例外などでは、finallyが実行されないこともあります。

finallyは、ファイルやストリームを閉じる、データベース接続を解放する、ロックを解除する、一時的な状態を元に戻すなど、確実に行いたい後処理に向いています。

ただし、IDisposableを実装しているリソースの解放には、現在のC#ではusing文を使うのが一般的です。using文は、finallyを簡潔に書くための仕組みとして理解するとよいでしょう。

finallyを使うときは、重い処理を書きすぎない、例外を発生させない、returnや不要なthrowを書かない、という点に注意することが大切です。

C#の例外処理を正しく理解するためには、trycatchfinallyの役割を分けて考えることが重要です。tryは通常処理、catchは例外処理、finallyは後処理と覚えておくと、読みやすく安全なコードを書けるようになります。