Javaの例外処理は、try-catch-finallyで例外を捕捉し、throw・throwsでスロー元と処理責任を分担し、必要に応じてカスタム例外でエラーの意味を明確にするという3つの基本を組み合わせて設計します。
つまずきやすいのはtry-catchの書き方そのものより「いつ・どの例外を・どう処理すべきか」の判断であり、この記事ではその判断軸を実務でそのまま使えるサンプルコード付きで解説します。
10年以上のJava開発経験で数多くのNullPointerExceptionやIOExceptionと格闘してきた筆者が、現場で本当に役立つ例外処理の知識を初心者にもわかりやすくまとめました。
読み終える頃には、catchブロックを空にする、Exceptionで安易に捕捉するといった「やってはいけない」例外処理を避けられるようになります。
Java例外処理の基本:例外とは何か

Javaでは、エラーの発生を避けて通れません。
Javaの例外処理は、エラーという予期せぬ事態に対応し、プログラムが突然停止するのを防ぐための重要な仕組みです。
まずは、例外処理の基本から見ていきましょう。
例外とは?なぜ必要なのか?
例外とは、プログラムの実行中に発生する「予期せぬできごと」や「エラー」を指します。例えば、存在しないファイルを開こうとしたり、数値をゼロで割り算しようとしたりする場合に発生します。
もし例外処理がなければ、プログラムはエラーが発生した時点で強制的に終了してしまいます。
強制終了すると、ユーザーが入力していたデータが失われたり、システム全体が不安定になります。
例外処理を適切に行うことで、以下のことが可能になります。
- プログラムの強制終了を防ぐ: エラーが発生しても、それを検知して代替処理を行い、プログラムの実行を継続させます。
- エラーの原因を特定する: 発生した例外の種類やメッセージから、どこで何が問題だったのかを特定しやすくなります。
- 安全な状態に復旧する: ファイルを閉じる、データベースの接続を元に戻すなど、エラー発生後もリソースを安全に解放できます。
システムの安定性と信頼性を高める上で、例外処理は不可欠な技術なのです。
例外の種類:Checked ExceptionとUnchecked Exception
Javaの例外は、大きく分けて2つの種類があります。Checked/Unchecked両方の特徴を理解することが、適切な例外処理を行う第一歩です。
実はCheckedException(チェック例外)という仕組みはJava特有の設計で、C#やKotlinなど後発の言語ではあえて採用されていません。
呼び出し元に対応を強制する構文が冗長になりやすいという批判があるためです。
筆者自身は、実務では「呼び出し元に対処を強制したいほど重要なエラーだけをCheckedにする」という基準で運用しています。
何でもCheckedにするとthrows句がメソッドシグネチャを埋め尽くし、かえって可読性が落ちるためです。
Checked Exception
コンパイル時にJavaコンパイラがチェックする例外です。
プログラマーに対して、try-catch 文で処理するか、throws 句で呼び出し元に処理を委ねることを強制します。
主に、プログラムの外部要因(ネットワーク、ファイルシステムなど)によって発生しうる、回復可能なエラーが該当します。
- 代表的な例:
IOException,SQLException,ClassNotFoundException
例えば、ファイルの読み込み処理を書く場合、そのファイルが存在しない可能性は常にあります。
そのため、Javaは IOException に対する処理をコンパイルの段階で要求するのです。
Unchecked Exception (実行時例外)
コンパイル時にはチェックされず、プログラムの実行時に発生する例外です。
RuntimeExceptionクラスとそのサブクラスがUnchecked Exceptionに該当します。
主に、プログラマーのコーディングミスや設計上の問題といった、プログラム内部の論理的な誤りが原因で発生します。
- 代表的な例:
NullPointerException,ArrayIndexOutOfBoundsException,IllegalArgumentException
nullのオブジェクトのメソッドを呼ぶとNullPointerExceptionが発生します。
Unchecked Exceptionは例外処理で対応するのではなく、コードを修正して防ぐべき問題です。そのためコンパイラは処理を強制しません。
筆者自身、新人時代はNullPointerExceptionを1日10回以上出していました。
Optionalの使い方を覚えてから、この頻度は激減しました。「NPEが多い人はOptionalを知らない」というのは、現場でよく聞くあるあるです。
nullチェックのif文を書く代わりにOptionalで「値がないかもしれない」ことを型で表現する習慣がつくと、NPEそのものを未然に防げるようになります。
よく使う例外クラス一覧
ここまでで登場したものを含め、Javaでよく遭遇する代表的な例外クラスを一覧にまとめました。
まずはこの一覧を眺めて「どの例外がChecked/Uncheckedのどちらに属するか」の感覚をつかんでおくと、以降の解説が理解しやすくなります。
| 分類 | 例外クラス | 発生原因 |
|---|---|---|
| Checked | IOException | ファイル入出力などI/O処理の失敗 |
| Checked | SQLException | データベースアクセス時のエラー |
| Checked | ClassNotFoundException | 指定したクラスが見つからない |
| Unchecked | NullPointerException | nullのオブジェクトへのメソッド呼び出し |
| Unchecked | ArrayIndexOutOfBoundsException | 配列の範囲外アクセス |
| Unchecked | IllegalArgumentException | メソッドに不正な引数を渡した |
| Unchecked | ClassCastException | 互換性のない型へのキャスト |
| Unchecked | ArithmeticException | ゼロ除算などの算術エラー |
try-catch文の基本構文
例外処理の最も基本的な構文が try-catch 文です。
例外が発生する可能性のある処理を try ブロックで囲み、発生した例外を catch ブロックで捕捉して対応します。
try {
// 例外が発生する可能性のある処理
int[] numbers = {1, 2, 3};
System.out.println(numbers[5]); // ArrayIndexOutOfBoundsExceptionが発生
} catch (ArrayIndexOutOfBoundsException e) {
// 例外を捕捉したときの処理
System.out.println("配列の範囲外にアクセスしました。");
System.out.println("エラー詳細: " + e.getMessage());
}
System.out.println("プログラムは正常に処理を続けました。");上の例では、配列の5番目(存在しない)にアクセスしてArrayIndexOutOfBoundsExceptionが発生します。
catchブロックで捕捉しているため、プログラムは強制終了しません。エラーメッセージを出力した後、後続の処理が実行されます。
try-catch-finallyの書き方【Java例外処理】

try-catch-finallyは、try(処理の実行)・catch(例外の捕捉)・finally(後処理)の3ブロックを組み合わせ、より柔軟で安全な例外処理を実装する構文です。
ここでは各ブロックの役割を順に解説します。
tryブロック:例外が発生する可能性のあるコード
try ブロックには、例外を発生させる可能性のあるコードを記述します。
ファイル操作、ネットワーク通信、データベース接続など、外部リソースを扱う処理が典型例です。
try {
// データベースへの接続や、ファイルの読み込み処理など
// この中で例外が発生すると、即座に実行が中断される
int result = 100 / 0; // ArithmeticExceptionが発生
System.out.println("この行は実行されません。");
} catch (ArithmeticException e) {
// ... catchブロックの処理 ...
}try ブロック内で例外が発生した瞬間、それ以降の処理は実行されません。
Javaの実行環境は、発生した例外に対応する catch ブロックを探し始めます。
catchブロック:例外を捕捉して処理する
catch ブロックは、try ブロックで発生した例外を捕捉し、その後の対応を記述する場所です。
catch の後の括弧 () には、捕捉したい例外のクラスを指定します。
複数の種類の例外に対応するために、catch ブロックを複数記述することも可能です。
その際は、より具体的な例外クラス(サブクラス)から順に記述する必要があります。
try {
// ... 例外が発生する可能性のある処理 ...
} catch (FileNotFoundException e) {
System.out.println("指定されたファイルが見つかりません。");
} catch (IOException e) {
System.out.println("ファイルの入出力エラーが発生しました。");
} catch (Exception e) {
System.out.println("予期せぬエラーが発生しました。");
e.printStackTrace(); // 開発時にエラーの詳細を確認するのに便利
}catch ブロックの引数で受け取った例外オブジェクト (e) から、エラーの詳細情報を取得できます。
getMessage() でエラーメッセージを、printStackTrace() でスタックトレース(メソッドの呼び出し履歴)を確認でき、デバッグに役立ちます。
catch ブロックを空にすることは避けてください。 エラーが検知されずに見過ごされ、後の工程でより深刻な問題を引き起こす原因となります。
筆者が新人時代に経験した事例ですが、あるプロジェクトで、空の catch ブロックが NullPointerException を握りつぶし、画面にエラーを表示しないまま不正なデータを保存する障害を起こしました。
原因調査に丸1日を費やし、最終的に catch ブロックにログ出力を追加したところ、発生箇所が即座に特定できました。
空の catch ブロックは、未来の自分やチームメンバーへの時限爆弾です。
マルチキャッチで複数の例外をまとめて処理する
Java 7以降では、複数の例外に対して同じ処理を行いたい場合、|(パイプ)演算子で例外クラスを結合する「マルチキャッチ」構文が使えます。
try {
// ... 例外が発生する可能性のある処理 ...
} catch (IOException | SQLException e) {
// IOExceptionとSQLExceptionを同じ処理で捕捉
System.out.println("外部リソースのエラーが発生しました: " + e.getMessage());
log.error("リソースエラー", e);
}前述の複数 catch ブロックとの使い分けは明確です。例外ごとに異なる処理が必要なら複数 catch、同じ処理でよいならマルチキャッチを使います。
ただし、継承関係にある例外(例: IOException と FileNotFoundException)を同時に指定するとコンパイルエラーになるため注意が必要です。
Java 22以降では、catchブロックで受け取った例外オブジェクトを使わない場合、変数名を_(アンダースコア)にできる「無名変数」構文が使えます。
try {
Integer.parseInt(token);
} catch (NumberFormatException _) {
// 例外の中身を使わない場合はこう書ける
System.out.println("数値に変換できませんでした。");
}従来は catch (NumberFormatException e) と書いても e を一切使わないケースがよくありましたが、_ を使うことで「この例外の詳細は使わない」という意図がコード上で明確になります。
finallyブロック:必ず実行される処理
finally ブロックは、try ブロックで例外が発生したかどうかにかかわらず、必ず最後に実行される処理を記述する場所です。
必ず実行される特性は、ファイルハンドルやデータベース接続といった、使用後に必ず解放しなければならないリソースの後処理に役立ちます。
FileReader reader = null;
try {
reader = new FileReader("sample.txt");
// ... ファイルの読み込み処理 ...
} catch (IOException e) {
System.out.println("ファイル処理中にエラーが発生しました。");
} finally {
if (reader != null) {
try {
reader.close(); // ファイルを閉じる
System.out.println("リソースを解放しました。");
} catch (IOException e) {
e.printStackTrace();
}
}
}この例では、try ブロックで処理が成功しても、catch ブロックでエラーが捕捉されても、finally ブロックは必ず実行されます。
finallyの実行により、readerが確実にクローズされ、リソースの解放漏れを防げます。
try-with-resources文でリソースを自動解放する
Java 7以降では、try-with-resources 文を使うことで、finally ブロックでの手動クローズが不要になります。
AutoCloseable インターフェースを実装したクラス(FileReader、BufferedReader、Connection 等)が対象です。
try の括弧内でリソースを宣言すると、ブロックを抜ける際に自動的に close() が呼ばれます。
// try-with-resourcesを使った例(Java 7以降)
try (FileReader reader = new FileReader("sample.txt");
BufferedReader br = new BufferedReader(reader)) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
System.out.println("ファイル処理中にエラーが発生しました。");
}
// finallyブロックもclose処理も不要!前述の finally で手動クローズする例と比べて、コードが大幅に簡潔になります。
複数のリソースをセミコロン区切りで宣言でき、宣言の逆順で自動クローズされます。
2026年時点の最新LTSであるJava 25でも、リソース管理には try-with-resources を使うのが標準的なベストプラクティスです。
Java 9以降では、try ブロックの外で宣言した実質final変数(事実上変更されない変数)をそのまま try-with-resources の括弧内で使えるようになりました。
Java 7〜8では try の括弧内で新たに変数を宣言する必要がありましたが、Java 9以降ではより柔軟な記述が可能です。
Javaの例外をスローする:throwとthrowsの違い

例外処理は、例外を捕捉するだけではありません。自ら例外を発生させたり、メソッドの呼び出し元に処理を委ねたりすることも重要なテクニックです。
例外を意図的に発生させる:throw文
throw 文を使用すると、プログラマーが意図的に例外を発生させられます。
これは、メソッドの引数が不正である場合など、特定の条件を満たしたときにプログラムをエラー状態にしたい場合に使用します。
public void setAge(int age) {
if (age < 0) {
// 不正な値が設定されようとしたため、例外をスローする
throw new IllegalArgumentException("年齢に負の値は設定できません。");
}
this.age = age;
}このメソッドは、引数 age が負の値の場合に IllegalArgumentException をスローします。
IllegalArgumentExceptionをスローすることで、不正なデータが設定されるのを防ぎ、呼び出し元に問題を通知できます。
メソッドがスローする可能性のある例外:throws句
throws 句は、メソッドのシグネチャ(定義部分)に記述し、そのメソッドが処理せずに呼び出し元へ投げる可能性のある Checked Exception を宣言するものです。
// このメソッドはIOExceptionをスローする可能性があることを宣言
public void readFile(String filePath) throws IOException {
FileReader reader = new FileReader(filePath);
// ... 読み込み処理 ...
reader.close();
}readFileの呼び出し側は、IOExceptionがスローされる可能性を認識する必要があります。
try-catchで処理するか、throws句でさらに上の呼び出し元へ処理を委譲します。
このように throws 句は、例外処理の責任を呼び出し元に明示的に伝える役割を果たします。
例外の再スロー
catch ブロックで例外を一度捕捉した後、ログ出力などの処理を行い、再び同じ例外や別の例外を throw することを「例外の再スロー」と呼びます。
public void processData() throws MySystemException {
try {
// ... 何らかの処理 ...
} catch (SQLException e) {
// エラーログを記録する
log.error("データベースアクセス中にエラーが発生しました。", e);
// より上位のアプリケーション例外に変換して再スローする
throw new MySystemException("システムエラーが発生しました。管理者に連絡してください。", e);
}
}再スローにより、下位レイヤーの技術的例外(SQLException等)を上位レイヤーのアプリケーション例外(MySystemException等)に変換できます。
システムの各層で関心事を分離でき、コードの保守性が高まります。
Javaカスタム例外の作り方と実践例

Javaが提供する既存の例外クラスだけでは、アプリケーション固有のエラー状況を十分に表現できないことがあります。
そのような場合に、独自の例外クラス(カスタム例外)を作成します。
なぜカスタム例外が必要なのか?
カスタム例外を作成する主な理由は、エラーの原因をより明確に伝えるためです。
例えば、銀行システムで「残高不足」が発生した場合、InsufficientBalanceExceptionという独自の例外を定義すれば、エラーの意味が格段にわかりやすくなります。
IllegalArgumentExceptionのような汎用例外では、何が起きたのか名前だけでは判断できません。
カスタム例外には、以下のメリットがあります。
- 可読性の向上: 例外クラス名だけで、どのようなエラーかが直感的に理解できます。
- 詳細なエラー情報: 独自のフィールドを追加して、残高や不足額などの詳細情報を例外に含められます。
- 柔軟なエラーハンドリング: 例外の種類ごとに
catchブロックを分け、それぞれに特化したエラー処理を実装できます。
カスタム例外クラスの作成方法
カスタム例外クラスの作成は非常に簡単です。
Exception クラス(Checked Exceptionの場合)または RuntimeException クラス(Unchecked Exceptionの場合)を継承するだけです。
エラーメッセージを受け取るコンストラクタと、原因となった例外を受け取るコンストラクタの2つを定義します。
// Checked Exceptionとしてカスタム例外を作成する例
public class InsufficientBalanceException extends Exception {
// エラーメッセージを受け取るコンストラクタ
public InsufficientBalanceException(String message) {
super(message);
}
// エラーメッセージと原因となった例外を受け取るコンストラクタ
public InsufficientBalanceException(String message, Throwable cause) {
super(message, cause);
}
}回復を試みるべきエラー(例: ネットワークの一時障害)はChecked Exceptionに、修正すべきバグ由来のエラー(例: 不正なデータ)はUnchecked Exceptionにする、という設計指針で判断します。
カスタム例外の利用例
作成したカスタム例外は、throw 文を使ってスローします。
public void withdraw(double amount) throws InsufficientBalanceException {
if (this.balance < amount) {
// 残高が足りない場合、カスタム例外をスローする
throw new InsufficientBalanceException("残高が " + (amount - this.balance) + " 円不足しています。");
}
this.balance -= amount;
System.out.println(amount + " 円を出金しました。");
}
// 呼び出し元の処理
try {
account.withdraw(50000);
} catch (InsufficientBalanceException e) {
System.out.println("出金エラー: " + e.getMessage());
}このようにカスタム例外を利用することで、アプリケーションの業務ロジックに沿った、わかりやすい例外処理を実現できます。
Java例外処理のベストプラクティス

最後に、品質の高いコードを書くための Javaの例外処理 に関するベストプラクティスをいくつか紹介します。
これらの指針を意識することで、より堅牢で保守性の高いアプリケーションを構築できます。
例外を適切に処理する
catchブロックを空にしない: 前述の通り、エラーを握りつぶす行為は問題の発見を遅らせる最大の原因です。最低でもログに出力するなど、何らかの対応を必ず行います。Exceptionで安易に捕捉しない:catch (Exception e)は、予期しない実行時例外まで捕捉してしまいます。可能な限り、IOExceptionやSQLExceptionのような、より具体的な例外クラスで捕捉するべきです。筆者の経験では、catch (Exception e)を多用したプロジェクトほどバグの原因特定が遅れました。NullPointerExceptionやClassCastExceptionといった本来修正すべきバグまで捕捉してしまい、問題の発見が本番リリース後にずれ込むケースが複数ありました。- 回復処理を検討する: 例外が発生したら、単にエラーメッセージを表示して終了するだけでなく、処理をリトライする、デフォルト値を返す、ユーザーに再入力を促すなど、アプリケーションを継続させるための回復処理を検討しましょう。
例外をログに出力する
エラーが発生した原因を調査する上で、ログは最も重要な情報源です。
- ロギングフレームワークを利用する:
e.printStackTrace()は、開発中のデバッグには便利ですが、コンソールに直接出力されるため、本番環境のログ管理には不向きです。SLF4JやLog4j2といった、標準的なロギングフレームワークの利用を強く推奨します(Log4j2は2021年のLog4Shell以降も脆弱性報告が継続しているため、必ず最新バージョンを使用し、定期的なアップデートを心がけてください)。 - 十分な情報を記録する: ログには、タイムスタンプ、エラーレベル(ERROR, WARNなど)、エラーメッセージ、そしてスタックトレースを含めるのが基本です。これにより、いつ、どこで、なぜエラーが発生したのかを後から追跡できます。
catch (SQLException e) {
// ログレベルERRORで、スタックトレースを含めて例外情報を記録
log.error("顧客データの取得に失敗しました。userID: {}", userID, e);
}独自の例外処理戦略を立てる
プロジェクトやアプリケーション全体で、例外処理に関する一貫したルール(戦略)を立てることが重要です。
- 例外の変換ルール: どの層で技術的例外(
SQLExceptionなど)を捕捉し、どの層で業務的例外(カスタム例外)に変換するのかを決めます。 - ログ出力のレベル: どのようなエラーを
ERRORレベルで記録し、どのようなものをWARNレベルで記録するのか、基準を明確にします。 - ユーザーへの通知方法: 例外が発生した際に、ユーザーにどのようなメッセージを表示するのか、画面遷移をどうするのか、といったUI/UXに関わる部分も設計に含めます。
これらの戦略をチームで共有し、コーディング規約として定めておくことで、アプリケーション全体の品質を均一に保ち、メンテナンス性を向上させられます。
筆者のチームでは、Repository層→Service層→Controller層と各層で例外を変換するルールを採用しています。
Repository層で SQLException をキャッチしたら DataAccessException に変換し、Service層で業務ルール違反なら BusinessException をスロー、Controller層で最終的にHTTPステータスコードにマッピングするという流れです。
このルールをチーム全員で共有してから、例外処理に関するコードレビュー指摘が大幅に減りました。
Javaの例外処理は、単なるエラー対応の技術ではなく、システムの安定性と信頼性を確保するための中心的な要素です。
try-catch-finallyの基本構造を理解し、throwとthrowsで責任を適切に分担しましょう。
カスタム例外でアプリ固有のエラーを明確に表現し、ログ出力や回復処理のベストプラクティスを日々のコーディングで実践することで、堅牢なJavaアプリケーションを開発できるようになります。
よくある質問
Q1. Javaの例外処理はなぜ必要ですか?
A1. プログラムの強制終了を防ぎ、安全にエラーへ対応するために必要です。例外処理がないと、エラー発生時にプログラムが即座に停止し、入力中のデータが失われたりシステムが不安定になったりします。
Q2. Checked ExceptionとUnchecked Exceptionの違いは何ですか?
A2. Checked Exceptionはコンパイル時にJavaコンパイラが処理を強制する例外(IOException等)で、Unchecked Exceptionはコンパイル時にチェックされず実行時に発生する例外(NullPointerException等)です。前者は外部要因、後者はコーディングミスが主な原因です。
Q3. try-catchとtry-catch-finallyの違いは何ですか?
A3. try-catchは例外の捕捉と対応のみを行いますが、try-catch-finallyのfinallyブロックは例外の有無にかかわらず必ず実行される処理を追加できます。ファイルやデータベース接続などリソースの後処理に使います。
Q4. throwとthrowsの違いは何ですか?
A4. throwは例外を実際に発生させる文で、throwsはメソッドのシグネチャに記述しそのメソッドが例外をスローする可能性があることを宣言する句です。throwは実行文、throwsは宣言という違いがあります。
Q5. カスタム例外はいつ作るべきですか?
A5. 既存の例外クラスだけではアプリケーション固有のエラー状況を十分に表現できない場合に作成します。例外クラス名だけでエラーの意味が伝わり、独自のフィールドで詳細情報を持たせられるメリットがあります。
Q6. catchブロックは空にしてもいいですか?
A6. いいえ、避けるべきです。空のcatchブロックはエラーを握りつぶし、後の工程でより深刻な問題を引き起こす原因になります。最低でもログ出力など何らかの対応を行ってください。
まとめ:Javaの例外処理を使いこなそう
この記事のポイントをまとめます。
- 例外処理はプログラムの強制終了を防ぎ、安全にエラーへ対応する仕組み
- Checked Exception → コンパイル時にチェックされる(
IOException等) - Unchecked Exception → 実行時に発生する(
NullPointerException等) try-catch-finallyで捕捉・対応・後処理を行うthrowで例外を発生させ、throwsで呼び出し元に処理を委譲する- カスタム例外でアプリ固有のエラーを明確に表現する
catchブロックを空にしない、具体的な例外で捕捉する、ログを記録する- try-with-resources(Java 7〜)やマルチキャッチ(Java 7〜)などモダンな構文も活用する
なお、2026年時点の最新LTSはJava 25(2025年9月リリース)です。
本記事で紹介したtry-catch・throw/throws・カスタム例外の基本構文は、Java 25でもそのまま通用します。




