Javaの例外処理完全ガイド|初心者向けにtry-catchからカスタム例外まで解説

Java入門・実践

「Javaの例外処理って、本当に大事ですよね。」

プログラムを書いていると、エラーは避けられません。

何も考えずにコードを書いていると、予期しないエラーで動作が止まり、「なんでこんなことに…?」と頭を抱えることになります。

でも、適切に例外処理をしておけば、エラーが発生してもスマートに対応できるんです。

実務でも、例外処理がしっかりしているシステムは安心感がありますし、逆に適当に扱われていると「このシステム、大丈夫かな…」と不安になります。

この記事では、初心者の方でも理解しやすいように、Javaの例外処理の基本からカスタム例外まで わかりやすく解説します。

プログラムをもっと安定したものにするために、ぜひ一緒に学んでいきましょう!

  1. はじめに 〜「例外処理」を軽く見ていたあの頃の私へ〜
  2. Javaの例外処理の基本 〜“エラーとの付き合い方”を知ろう〜
    1. 例外(Exception)とは?
    2. 正常系 vs. 異常系
    3. Javaにおける例外の種類
      1. チェック例外(Checked Exception)
      2. 非チェック例外(Unchecked Exception)
    4. Javaでの基本的な例外処理の書き方
  3. try-catchの使い方 〜Javaの例外処理、まずはここから〜
    1. 基本の構文を理解しよう
    2. catchブロックでは何をすべき?
    3. finallyブロックでリソースをきちんと解放
    4. 複数の例外を処理するには?
    5. まとめ:try-catchはコードの「安全装置」
  4. throwsと例外の伝播 〜例外をキャッチしないという選択〜
    1. throwsって何をしてるの?
    2. throwsはいつ使う?
    3. 呼び出し元での対処法
    4. throwsするか try-catchするかの判断ポイント
    5. 複数の例外をthrowsすることもできる
    6. まとめ:throwsで「責任の分担」を意識しよう
  5. カスタム例外とは?〜伝えたいことは自分で作る〜
    1. そもそもカスタム例外ってなに?
    2. 基本の作り方
    3. Checked or Unchecked? どちらを継承すべき?
    4. 実務的な活用パターン
    5. カスタム例外を設計するときのポイント
    6. まとめ:例外クラスにも「語彙力」を
  6. 例外処理のベストプラクティス 〜書いた人も、読む人も安心できるコードへ〜
    1. よくあるミス①:catchで例外を握りつぶす
    2. よくあるミス②:Exceptionで全部キャッチする
    3. ベストプラクティス①:適切なエラーメッセージを設計する
    4. ベストプラクティス②:ログの粒度を意識する
    5. ベストプラクティス③:例外処理の「責任の分離」を意識する
    6. ベストプラクティス④:再試行や代替策を検討する
    7. まとめ:例外処理は“保険”じゃなく“設計”の一部
  7. 実践編|サンプルコードで学ぶ 〜“読まれるコード”と“直されるコード”の分かれ道〜
    1. ケース①:ファイル読み込みの例
      1. 悪い例
      2. 良い例
    2. ケース②:ユーザー検索処理
    3. 良いコードに共通するポイント
    4. 例外処理と美しさ
  8. まとめと次のステップ 〜エラーを恐れず、育てるコードへ〜
    1. 例外処理を恐れない、むしろ活かす
    2. 実務で使える!活かしどころリスト
    3. 次のステップ

はじめに 〜「例外処理」を軽く見ていたあの頃の私へ〜

プログラミングを始めた頃、「try-catchってとりあえず書いとけばいいんでしょ?」くらいに考えていました。

とにかくエラーが出ないようにして、動けばOK。

実行中に赤いエラーが出るとビビって、catchブロックの中にe.printStackTrace();を放り込んで、「ふぅ…とりあえず終わった」と安心していた、そんな時期もありました。

でも、実務に入ってから状況は一変します。

「例外処理が甘いとシステム全体の信頼性が揺らぐ」という現実に何度も直面しました。

ログに肝心の情報が残っていなかったり、ユーザーに冷たいエラーメッセージが表示されたり…。

ときには原因が分からず、何時間もデバッグに費やすこともありました。

それ以来、例外処理への向き合い方が変わりました。

コードの品質や保守性、ユーザーの体験までも左右するこの「縁の下の力持ち」こそ、きちんと理解して設計する価値のあるテーマなのだと感じています。

この章では、これからJavaを学ぶ方や、例外処理について「何となくわかっているけど自信がない」という方に向けて、例外処理がなぜ重要なのか、どうして避けて通れないテーマなのかをお伝えします。

そして、本記事の全体像やゴールも紹介していきます。

一緒に、「try-catchを書いておけばなんとなく大丈夫」から卒業して、エラーに強い、美しいコードを目指しましょう!

Javaの例外処理の基本 〜“エラーとの付き合い方”を知ろう〜

プログラミングにおいて、「エラーが起きたら終了」では、信頼性のあるアプリケーションは作れません。

エラーが起きても、ちゃんと受け止めて、必要な対応を行う

──それが例外処理(Exception Handling)です。

ではまず、「そもそも例外って何なの?」というところから、丁寧に見ていきましょう。

例外(Exception)とは?

通常、プログラムは上から順に命令を実行していきますが、想定外の事態が発生すると、そこで処理が中断されてしまいます。たとえば…

  • 存在しないファイルを開こうとしたとき
  • 0で割り算しようとしたとき
  • ネットワークに接続できなかったとき

…こういった場面で発生するのが「例外」です。

英語で Exception と呼ばれる通り、「例外的な事態」なんですね。

これらはプログラムにとって“イレギュラー”な出来事であり、無視してしまうとアプリが落ちてしまうこともあります。

正常系 vs. 異常系

ここで一度、プログラムの動き方を整理してみましょう。

  • 正常系(Happy Path):想定通りにすべてが進むパターン
  • 異常系(例外パス):何かエラーが起きて、それに対処する必要があるパターン

実際の開発では、異常系にどれだけ丁寧に対応できるかが、コードの質を左右します。

ユーザーから見ても、エラーが起きたときに「ごめんなさい、通信できませんでした」のように丁寧な案内があると、安心できますよね。

Javaにおける例外の種類

Javaでは、例外は「チェック例外」と「非チェック例外」の2つに分類されます。

チェック例外(Checked Exception)

  • コンパイル時にチェックされる
  • try-catchで囲むか、throwsでスローしないとコンパイルエラーになる
  • 例:IOException, SQLException, FileNotFoundException

→ ファイル操作やデータベース接続など、起こり得る問題を確実にハンドリングしてね!というJavaの親心。

非チェック例外(Unchecked Exception)

  • 実行時に発生する
  • コンパイラは「try-catchしなさい」とは言ってこない
  • 例:NullPointerException, ArrayIndexOutOfBoundsException, IllegalArgumentException

→ 主にプログラミングのミスによるものなので、設計やテストで予防するのが基本方針。

この分類を知っておくと、どんな場面で例外処理が必要か、判断しやすくなります。

Javaでの基本的な例外処理の書き方

Javaでは例外処理に try-catch-finally という構文を使います。

Java
try {
    // 例外が起きそうなコード
} catch (例外の型 e) {
    // 例外が発生したときの処理
} finally {
    // 必ず実行したい処理(省略可)
}
  • try ブロックでリスクのある処理を囲み、
  • catch ブロックで例外をキャッチして適切に処理、
  • finally ブロックでは、たとえばリソースの解放など、例外が起きても起きなくても実行したい処理を書くことができます。

この構文をうまく使いこなすことで、アプリは安定性と信頼性を手に入れるのです。

次章では、具体的な try-catch の使い方を、コード付きで詳しく解説していきます。

ここまでで「例外処理はただの“エラー避け”じゃない」と感じてもらえたらうれしいです。

try-catchの使い方 〜Javaの例外処理、まずはここから〜

さて、前の章で「例外処理の大切さ」と「Javaにおける例外の種類」について学びました。

ここからは、実際に例外処理を書くときに欠かせない構文、try-catchの使い方を見ていきましょう。

エラーと向き合う第一歩は、まずtry-catchを正しく使いこなすことから始まります。

基本の構文を理解しよう

Javaで例外処理を書くときの基本形はこんな感じです。

Java
try {
    // 例外が発生するかもしれない処理
} catch (例外の型 変数名) {
    // 例外が発生したときの処理
}

たとえば、ファイルを開くコードはこんな風になります:

Java
try {
    FileReader reader = new FileReader("data.txt");
    // ファイルの読み取り処理
} catch (FileNotFoundException e) {
    System.out.println("ファイルが見つかりませんでした");
}

このように、tryブロック内に「リスクのある処理」を書き、エラーが発生したときにcatchでフォローするのが基本です。

catchブロックでは何をすべき?

ただprintStackTrace()を呼ぶだけでは、情報が不十分なことも。

以下のような考え方をすると、より実践的です。

  • ユーザー向けに やさしいエラーメッセージ を表示する
  • ログファイルに 詳細なスタックトレースやエラー情報 を残す
  • 再試行やフォールバックの処理を行う(場面に応じて)

例:

Java
catch (FileNotFoundException e) {
    System.out.println("指定されたファイルが存在しません。パスをご確認ください。");
    logger.error("ファイルオープンに失敗しました: " + e.getMessage());
}

finallyブロックでリソースをきちんと解放

例外が起きても起きなくても必ず実行されるのがfinallyブロックです。

主に「リソースの解放(ファイル・データベース接続など)」に使います。

Java
FileReader reader = null;
try {
    reader = new FileReader("data.txt");
    // 読み取り処理
} catch (IOException e) {
    System.out.println("読み込み中にエラーが発生しました");
} finally {
    if (reader != null) {
        try {
            reader.close();
        } catch (IOException e) {
            // エラー処理
        }
    }
}

リソースリーク(=解放漏れ)を防ぐためにも、finallyの活用はとても大切です。

複数の例外を処理するには?

1つのtryブロックで、異なる種類の例外が起きる可能性もあります。

その場合はcatchブロックを複数用意することで対応できます。

Java
try {
    // 例外が複数起きるかもしれない処理
} catch (IOException e) {
    // 入出力系のエラー処理
} catch (NullPointerException e) {
    // Null参照時の処理
}

このように、例外の種類ごとに適切な処理を分けることができます。

まとめ:try-catchはコードの「安全装置」

例外処理の要、try-catchは、まさにコードのエアバッグのようなもの。

事故(=エラー)が起きても、被害を最小限に食い止めてくれる仕組みです。

ただし、乱用するとコードが読みにくくなったり、例外が握りつぶされて原因不明になったりするので、適切な使い方がカギです。

次の章では、throwsを使って例外を呼び出し元に渡す方法を見ていきます。

設計の幅が広がる考え方なので、ぜひお楽しみに!

throwsと例外の伝播 〜例外をキャッチしないという選択〜

Javaでは、例外が発生したときに try-catch でその場で処理する以外に、「例外をそのまま呼び出し元に投げ返す」という方法もあります。

それが throwsを使った「例外の伝播」です。

throwsって何をしてるの?

throwsは、メソッドが例外を「自分では処理せず、呼び出した側に伝える」ときに使います。

Java
public void readFile() throws IOException {
    FileReader reader = new FileReader("data.txt");
    // 読み取り処理
}

このように書くことで、「このメソッド内では IOException が発生する可能性があるよ」と明示的に伝えることができます。

つまり、

  • try-catch → 今ここで処理する
  • throws → 呼び出した人に処理を任せる

…という役割分担なんですね。

throwsはいつ使う?

以下のような場面で throws を使うと効果的です。

  • 呼び出し元で例外をまとめて処理したい → 複数メソッドで同じ例外が発生するなら、一箇所でハンドリングした方がスッキリ!
  • 処理の責任を呼び出し元に明確にする設計にしたい → APIやライブラリ設計では、開発者が例外を意識できるようにするのが親切。
  • 今のメソッドでは適切な対応ができない → その場しのぎでキャッチするより、責任ある場所で処理した方が安全。

呼び出し元での対処法

throwsされた例外は、呼び出し元で try-catch することで処理できます。

Java
public static void main(String[] args) {
    try {
        readFile();  // throwsされたメソッドを呼び出す
    } catch (IOException e) {
        System.out.println("ファイル読み込みに失敗しました:" + e.getMessage());
    }
}

このように、例外のハンドリングをより上位の層にゆだねることで、責任ある位置での対応が可能になります。

throwsするか try-catchするかの判断ポイント

初心者が悩みがちなポイントなので、判断のヒントを整理しておきます。

処理する場所向いている場面
try-catch例外をすぐに対処できる / ユーザーに適切なフィードバックを返したい
throwsもっと上の層でまとめて処理したい / 自分では対応方法が分からない

例外処理は「誰が責任を持って対応するか」を設計することだと考えると、判断しやすくなります。

複数の例外をthrowsすることもできる

1つのメソッドが複数の例外をスローする場合も、カンマ区切りで書けます。

Java
public void process() throws IOException, SQLException {
    // 複数の例外が発生する処理
}

ただし、例外が多すぎると逆に読みにくくなるので、可能であれば例外の抽象度を上げる(または別メソッドに分ける)ことも検討しましょう。

まとめ:throwsで「責任の分担」を意識しよう

例外の伝播を学ぶことで、Javaの例外処理の設計がぐっと柔軟になります。

try-catchだけが正解ではなく、状況に応じて「自分で処理するか」「誰かに任せるか」を選べるようになるのが大事なポイントです。

次章では、いよいよ「自分で作る例外=カスタム例外」について学んでいきます。システムの要件にピタッと合ったエラー処理を設計できるようになりますよ!

カスタム例外とは?〜伝えたいことは自分で作る〜

システムを開発していて、「この処理、うまくいかなかったけど、NullPointerExceptionじゃなんかピンとこないな…」と思ったことはありませんか?

そうなんです。標準の例外クラスでは、意図や状況を“伝えきれない”ことがあるんです。

そこで登場するのが、「カスタム例外(ユーザー定義例外)」です。

そもそもカスタム例外ってなに?

Javaの例外クラスは継承可能なので、自分だけのオリジナル例外クラスを作ることができます。

つまり、処理の文脈に合った例外を“名前付き”で定義できるということです。

たとえば、「ユーザーが見つからない」という状況に、NoSuchElementExceptionを使っても動きますが、UserNotFoundException という名前にすると、何が問題か一目瞭然ですよね。

基本の作り方

カスタム例外は通常、Exception または RuntimeException を継承して作成します。以下は基本形です。

Java
public class UserNotFoundException extends Exception {
    public UserNotFoundException(String message) {
        super(message);
    }
}

これだけでOK! あとは、例外を発生させたいタイミングで throw すれば使えます。

Java
public void findUser(String username) throws UserNotFoundException {
    if (/* ユーザーが存在しない */) {
        throw new UserNotFoundException("ユーザー " + username + " が見つかりませんでした。");
    }
}

Checked or Unchecked? どちらを継承すべき?

例外クラスを作るとき、「Exceptionを継承するか? RuntimeExceptionか?」で迷いますよね。

継承先特徴
Exception(Checked)呼び出し元で throws 宣言が必須IOException, SQLException
RuntimeException(Unchecked)呼び出し元で throws 宣言なしでも使える(任意)NullPointerException, IllegalArgumentException
  • 業務的な失敗(例:銀行口座が存在しないなど) → Exception を継承(確実に対処すべき)
  • プログラミングミス寄り(例:引数が不正など) → RuntimeException を継承

この判断は設計方針にもよるので、プロジェクトやチームルールにあわせて選びましょう。

実務的な活用パターン

カスタム例外は、以下のようなケースで使うと効果的です。

  • 複雑な処理フローの中で、明確なエラー状態を伝えたい
  • 同じ種類のエラーをまとめてキャッチしたい
  • ログやモニタリングの粒度を細かくしたい(→例外のクラス名で意味を区別)

例:

Java
catch (UserNotFoundException e) {
    logger.warn("ユーザーが存在しません: " + e.getMessage());
    // ログレベルや通知を変える判断がしやすい!
}

カスタム例外を設計するときのポイント

  • クラス名は「○○Exception」と明確に
  • エラーの文脈を伝えるメッセージを含める
  • 場合によっては、エラーコードや独自フィールドを追加するのもアリ!
Java
public class BusinessException extends Exception {
    private int errorCode;

    public BusinessException(String message, int code) {
        super(message);
        this.errorCode = code;
    }

    public int getErrorCode() {
        return errorCode;
    }
}

まとめ:例外クラスにも「語彙力」を

カスタム例外を使うことで、コード全体の読みやすさと保守性がぐっとアップします。

「なぜ失敗したのか」が一目でわかるクラス名・メッセージは、チーム開発やリーダブルコードの鍵にもなります。

次章では、実際によくあるミスや、実務でありがちな“例外処理のやらかし”を紹介しつつ、ベストプラクティスを掘り下げていきますよ!

例外処理のベストプラクティス 〜書いた人も、読む人も安心できるコードへ〜

例外処理の構文や仕組みがわかってくると、「とりあえずtry-catchで囲んでおけばいいかな」と思いがちです。

でも、本当に大切なのはそこから先。

コードの可読性、保守性、実際の運用に耐えうるエラーハンドリングをどう書くかで、システムの安心感がガラッと変わります。

この章では、実務で「うわ、これ直すの大変…」という例に出会った方にも響くような、よくある例外処理のミスとその回避法を紹介していきます。

よくあるミス①:catchで例外を握りつぶす

Java
try {
    // なにか処理
} catch (Exception e) {
    // 何も書かない、または空のcatch
}

これは例外処理の“無視”にあたります。

ログにも残らず、何が起きたのか誰にもわからないため、問題の特定が非常に困難になります。

改善ポイント:

  • 最低でもログに残す
  • 可能であれば、ユーザーや上位層に情報を伝える
  • 無理に処理を続けず、安全にリカバリまたは停止する判断も必要

よくあるミス②:Exceptionで全部キャッチする

Java
catch (Exception e) {
    // 例外すべてを一括処理
}

一見シンプルですが、予期せぬ例外まで“なんでもかんでも”飲み込んでしまう危険があります。

改善ポイント:

  • 発生し得る具体的な例外を明示してキャッチ
  • 最上位層でのみ Exception をキャッチするケースはOK(=ログやモニタリングに使うなど)

ベストプラクティス①:適切なエラーメッセージを設計する

例外発生時のメッセージは、開発者や運用者が真っ先に見る情報です。

「nullだった」「失敗しました」ではなく、状況と文脈を含んだメッセージを意識しましょう。

Java
throw new UserNotFoundException("ユーザーID 'abc123' が見つかりませんでした");

→ 誰が、何を、どうしようとして、なぜダメだったのかが一目瞭然!

ベストプラクティス②:ログの粒度を意識する

例外が発生した際、適切なログ出力がされていないと、トラブル対応が難航します。

  • ログレベルは適切に(warn, error, info)
  • スタックトレースは必要に応じて出力(e.printStackTrace()の代わりにロガーを使う)
  • 同じ例外でもケースごとにログメッセージを変える工夫も有効です

ベストプラクティス③:例外処理の「責任の分離」を意識する

  • ビジネスロジックの中でUI向けのエラーメッセージを組み立てるのはNG
  • 例外の発生とその表示・通知は層ごとに役割を分ける(例:ドメイン層とUI層で役割分担)

→ システムが大きくなっても柔軟に拡張・修正が可能になります。

ベストプラクティス④:再試行や代替策を検討する

たとえばネットワークや外部APIの呼び出しで例外が発生する場合、即エラー扱いではなく、「リトライ」「キャッシュ」「フォールバック」といった手法も選択肢に入ります。

→ 「失敗したら終わり」ではなく、「失敗したときどうするかまで含めて設計」できると、プロっぽさが増しますね!

まとめ:例外処理は“保険”じゃなく“設計”の一部

例外処理は「失敗したときの逃げ道」と思われがちですが、実際にはシステム設計そのものの質を左右する要素です。

「例外をどう扱うか」は「信頼性・保守性・チームのストレス」にダイレクトに影響します。

次の章では、具体的なユースケースとサンプルコードを使って、“良い例”と“悪い例”を比較しながら、さらに実践的な知識に落とし込んでいきます!

実践編|サンプルコードで学ぶ 〜“読まれるコード”と“直されるコード”の分かれ道〜

例外処理の知識がついてくると、コードを書きながら「これでいいのかな?」と不安に感じる瞬間も出てきます。

この章では、例外処理の「良い例」「悪い例」を対比しながら、より読みやすく・保守しやすいコードを書くためのポイントを確認していきましょう。

ケース①:ファイル読み込みの例

悪い例

Java
try {
    FileReader reader = new FileReader("data.txt");
    // ファイル処理...
} catch (Exception e) {
    // とにかく握りつぶす
}
  • どんな例外が起きたのか分からない
  • ログもなく、デバッグが困難
  • FileReaderはIOExceptionをスローするので、catchは型を絞るべき

良い例

Java
try (FileReader reader = new FileReader("data.txt")) {
    // ファイル処理...
} catch (FileNotFoundException e) {
    System.err.println("ファイルが見つかりません: " + e.getMessage());
} catch (IOException e) {
    System.err.println("ファイルの読み込み中にエラーが発生しました: " + e.getMessage());
}
  • 具体的な例外をcatch
  • 原因の違いをログ出力で明示
  • try-with-resourcesによりfinallyでのcloseが不要に

ケース②:ユーザー検索処理

Java
public User findUser(String userId) throws UserNotFoundException {
    User user = userRepository.get(userId);
    if (user == null) {
        throw new UserNotFoundException("ID: " + userId + " のユーザーが見つかりません");
    }
    return user;
}

呼び出し元:

Java
try {
    User user = userService.findUser("taro123");
    System.out.println("ようこそ、" + user.getName() + "さん!");
} catch (UserNotFoundException e) {
    System.err.println(e.getMessage());
}

このように、ドメインに即したカスタム例外+文脈を含むメッセージは、開発者にもユーザーにもやさしい設計ですね。

良いコードに共通するポイント

  • catchの型が明確 → どんな異常にどう対応するかが読みやすい
  • メッセージが具体的 → 「なぜ失敗したか」が直感的にわかる
  • 責任の分離がはっきりしている → 例外を投げるメソッドと処理する場所が明確
  • ログ or ユーザー通知が適切 → 関係者が困らない

例外処理と美しさ

例外処理は、地味で裏方のように思われがちです。

でも私は、例外処理こそ“読み手へのやさしさ”が詰まったコードだと思っています。

例外の設計とメッセージから、その人の開発姿勢まで見えてくる。そんな気がするんです。

まとめと次のステップ 〜エラーを恐れず、育てるコードへ〜

この記事では、Javaにおける例外処理の基本から応用まで、段階を追って学んできました。

  • 例外処理の役割とは?
  • try-catch構文の正しい使い方
  • throwsによる例外の伝播と設計の考え方
  • カスタム例外の作り方と活用場面
  • 書いて安心される例外処理 vs. 書き直される例外処理
  • 実務に効く例外処理のベストプラクティス
  • そして、それらを組み合わせた“伝わるコード”の実例たち

思い返せば、「とりあえずcatchで握りつぶす」から「自信を持って設計できる例外処理」まで、ずいぶん遠くに来ましたね。

例外処理を恐れない、むしろ活かす

エラーは“悪”ではありません。

むしろ、うまく扱えばアプリをより安全に・よりユーザーにやさしくしてくれる強力な味方です。

「どうせ落ちるなら、きれいに落ちよう」 そんな姿勢こそが、結果として「落ちにくくて、育てやすいコード」を生むと私は信じています。

実務で使える!活かしどころリスト

  • 外部API連携 → ネットワーク例外・タイムアウトに備えたリトライ設計
  • ファイル操作・DB操作 → Checked Exceptionを明示的に処理
  • バリデーション → カスタム例外を使って、意味のあるフィードバックを返す
  • エラーログ → ログレベルの最適化とスタックトレースの活用

記事を読みながら書いたメモがあれば、ぜひご自身のプロジェクトや過去のコードに照らし合わせてみてください。

次のステップ

学びを定着させるには、“誰かに教える・アウトプットする”のが一番効果的。ブログや技術記事で解説してみるのも大きな力になります!

さらにレベルアップしたい方に向けて、こんな次のテーマもおすすめです:

  • try-with-resources構文とリソース管理の自動化
  • Spring Bootにおけるグローバル例外ハンドリング(@ControllerAdvice)
  • ログライブラリ(Logback, SLF4Jなど)と連携したエラーハンドリング設計
  • JUnitでの例外を含むテストの書き方
decopon
decopon

エラーはプログラムの落とし穴ではなく、成長のチャンスです。 その“声”にちゃんと耳を傾けるコードは、開発者にとってもユーザーにとっても信頼できる存在になります。

この記事が、開発者の手助けとなり、「例外処理ってなんだか楽しいかも」と思えるきっかけになれば嬉しいです。

コメント

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