1. 👋 はじめに
前回はコメント・ドキュメント・マジックナンバーの排除を学びました。
今回は エラーハンドリング を解説します!
「エラーは起きるもの。問題はいつ・どこで・どう気づくかだ。」
プログラムは必ず想定外の状況に直面します。
そのとき「どう対応するか」を事前に設計しておくのが
エラーハンドリングです。
この記事を読めば:
- ✅ 例外処理(try/except)の基本がわかる
- ✅ 良いエラーメッセージの書き方がわかる
- ✅ 防御的プログラミングの考え方がわかる
- ✅ カスタム例外の作り方がわかる
2. 🚨 エラーと例外の違い
まず用語を整理しましょう。
| 種類 | 説明 | 例 |
|---|---|---|
| エラー(Error) | 修復不可能な問題 | メモリ不足・スタックオーバーフロー |
| 例外(Exception) | 想定内の異常事態・適切に対処できる | ファイルが見つからない・入力値が不正 |
⚠️ 補足:言語によって用語の扱いは異なります
上の表は「概念的な整理」であり、すべての言語に当てはまるわけではありません。
- Python では「エラー ⊃ 例外」という包含関係で、「構文エラー」と「例外」の2種類として説明されています(Python公式ドキュメント)
- JavaScript では
Errorオブジェクトが例外を表すなど、「Error」という名前で例外を扱います(MDN)「回復可能か否か」「例外機構を使うか否か」は言語・ライブラリの設計次第です。
このシリーズでは Python と TypeScript を中心に 例外処理を解説していきます。
プログラマーが主に扱うのは「例外」です。
例外は「起きてほしくないが、起きることが想定される事態」に対処するための仕組みです。
3. 🔰 例外処理の基本(try/except・try/catch)
Pythonの例外処理
# 基本構文
try:
# 例外が発生するかもしれない処理
result = 10 / 0
except ZeroDivisionError:
# 例外が発生したときの処理
print("ゼロで割ることはできません")
各ブロックの役割
try:
# 例外が発生するかもしれない処理
file = open("data.txt", "r")
content = file.read()
except FileNotFoundError:
# 特定の例外が発生したときの処理
print("ファイルが見つかりません")
except PermissionError:
# 別の例外が発生したときの処理
print("ファイルを読む権限がありません")
except Exception as e:
# 上記以外の予期しない例外
print(f"予期しないエラーが発生しました: {e}")
else:
# 例外が発生しなかったときだけ実行
print(f"読み込み成功:{len(content)}文字")
finally:
# 例外の有無にかかわらず必ず実行(後片付け)
file.close()
| ブロック | 実行タイミング | 主な用途 |
|---|---|---|
try |
常に実行 | 例外が起きうる処理 |
except |
例外が起きたとき | エラー処理・メッセージ表示 |
else |
例外が起きなかったとき | 成功時の後処理 |
finally |
常に実行(例外有無問わず) | リソースの解放・後片付け |
💡
finallyはreturnをしても必ず実行される!def read_file(path: str) -> str: file = open(path, "r") try: return file.read() # ← return しようとしても… finally: file.close() # ← 必ずここを通ってから関数を抜ける!
tryの中でreturnをして関数を終了しようとしても、
finallyの中身は必ず先に実行されてから関数を抜けます。
だからこそ「ファイルのクローズ・DB接続の切断」といった
後片付けに最適なブロックです。
例外の伝播(バブルアップ)
💡 例外はキャッチされるまでプログラムを「駆け上がる」
関数の中で例外が発生したとき、その関数内に
try-exceptがなければ
例外は呼び出し元の関数へと上に向かって伝播(バブルアップ) していきます。
どこにもキャッチされないと最終的にプログラムがクラッシュします。
実際のコードで見てみましょう。
# 国コードごとの消費税率を定義した辞書(定数)
TAX_RATES = {
"JP": 0.10, # 日本:10%
"US": 0.08, # アメリカ:8%(州によって異なるため例)
"DE": 0.19, # ドイツ:19%
}
def get_tax_rate(country: str) -> float:
"""国の消費税率を返す。"""
if country not in TAX_RATES:
raise ValueError(f"未対応の国コードです: {country}") # ① ここで例外発生!
return TAX_RATES[country]
def calculate_price(price: int, country: str) -> int:
"""税込価格を計算する。"""
# try-except がないので例外がそのまま上に伝わる
tax_rate = get_tax_rate(country) # ② get_tax_rate から例外が飛んでくる
return int(price * (1 + tax_rate)) # ← ここには到達しない
def process_order(order: Order) -> None:
"""注文を処理する。"""
# try-except がないので例外がさらに上に伝わる
total = calculate_price(order.price, order.country) # ③ calculate_price から例外が飛んでくる
save_order(order, total) # ← ここには到達しない
def main() -> None:
"""メイン処理。"""
try:
process_order(my_order) # ④ ここでようやくキャッチ!
except ValueError as e:
print(f"注文処理エラー: {e}")
【例外が伝播するイメージ】
main() ← ④ここでキャッチ!
└─ process_order() ← ③キャッチなし → 上に伝播
└─ calculate_price() ← ②キャッチなし → 上に伝播
└─ get_tax_rate() ← ①ここで ValueError が発生!
例外は泡(バブル)のように下から上へと浮かび上がっていく
→ だから「バブルアップ」と呼ぶ!
どこでキャッチすべきか?
キャッチする場所 向いているケース 発生した関数のすぐそば すぐに回復できる(リトライ・デフォルト値を使うなど) 上位の処理フロー まとめてエラー処理・ログ記録をしたい場合 アプリケーションの入り口 ユーザーへのエラーメッセージ表示・クラッシュ防止 原則:「対処できる場所でキャッチする」
何もできないのにキャッチするのは握りつぶしと同じです。
対処できないなら上に伝播させましょう。
TypeScriptの例外処理
// TypeScript の基本構文
try {
const result = JSON.parse(invalidJson);
} catch (error) {
if (error instanceof SyntaxError) {
console.error("JSONの形式が正しくありません");
} else {
console.error("予期しないエラーが発生しました:", error);
}
} finally {
console.log("処理完了");
}
💡 TypeScriptの
catchで受け取るエラーはunknown型!TypeScript 4.4以降、
catch (error)のerrorの型はunknown(何が来るかわからない)です。
そのままではerror.messageのようにプロパティにアクセスできません。catch (error) { console.log(error.message); // ❌ unknown型なのでアクセスできない! }そこで
instanceofを使って「もしこのエラーが〇〇だったら」と
型を特定(型ガード) してからアクセスするのがクリーンな書き方です。catch (error) { if (error instanceof SyntaxError) { console.log(error.message); // ✅ SyntaxError と確定したのでOK } }Pythonの
except ValueError as e:と違い、TypeScriptでは
instanceofでの型ガードが必須になります。
4. ❌ 悪いエラーハンドリングのパターン
パターン①:すべての例外を握りつぶす
# ❌ 例外を完全に無視している(最悪のパターン)
try:
result = process_data(data)
except:
pass # 何も起きていないふりをする 😱
# ❌ エラーを握りつぶして None を返す
def get_user(user_id: int) -> User | None:
"""ユーザーを取得する。"""
try:
return db.find(user_id)
except Exception:
return None # エラーが起きても呼び出し側にはわからない 😱
⚠️ 例外を握りつぶすのは最もやってはいけないパターンです。
バグが発生してもどこで何が起きたかわからなくなり、
デバッグが非常に困難になります。
パターン②:エラーメッセージが不親切
# ❌ 何が問題かわからないエラーメッセージ
raise ValueError("エラー")
raise Exception("失敗しました")
raise RuntimeError("Something went wrong")
# ✅ 何が・なぜ・どうすれば解決できるかがわかるメッセージ
raise ValueError(
f"年齢の値が不正です: {age}。"
"0以上150以下の整数を入力してください。"
)
raise FileNotFoundError(
f"設定ファイルが見つかりません: {config_path}。"
"README.mdの手順に従って設定ファイルを作成してください。"
)
パターン③:例外をキャッチして再度raiseしない
# ❌ 例外をキャッチしても何もせず再スローしている(意味がない)
try:
result = risky_operation()
except Exception as e:
raise e # キャッチした意味がない
# ✅ 必要な情報を追加して再スローする(コンテキストを追加)
try:
result = risky_operation()
except Exception as e:
raise RuntimeError(
f"ユーザー {user_id} のデータ処理中にエラーが発生しました"
) from e # 元の例外を原因として保持する
5. ✅ 良いエラーハンドリングのパターン
パターン①:具体的な例外をキャッチする
# ❌ 広すぎる例外キャッチ
try:
data = fetch_data()
except Exception:
handle_error()
⚠️
except Exceptionが広すぎる理由:バグまで隠してしまう!try: data = fetch_daat() # ← タイポ!「fetch_daat」は存在しない result = proces(data) # ← タイポ!「proces」は存在しない except Exception: handle_error() # ← NameError も AttributeError も # すべて飲み込んでしまう 😱
NameError(存在しない名前を参照)やAttributeError(存在しないメソッドを呼び出し)
のようなプログラマー自身のバグまで一緒にキャッチして隠してしまいます。
バグがあってもhandle_error()が呼ばれるだけでクラッシュせず、
原因の発見が大幅に遅れます。
# ✅ 想定される例外を具体的にキャッチする
try:
data = fetch_data()
except ConnectionError:
# ネットワークエラーは再試行できる
retry_fetch()
except TimeoutError:
# タイムアウトはキャッシュを使う
use_cached_data()
except ValueError as e:
# データ形式エラーはログに記録して報告
logger.error(f"データ形式エラー: {e}")
raise
パターン②:早期リターンで正常系を明確にする
# ❌ ネストが深くて読みにくい
def process_user(user_id: int) -> str:
"""ユーザーを処理する。"""
if user_id is not None:
user = get_user(user_id)
if user is not None:
if user.is_active:
return user.name
else:
raise ValueError("非アクティブなユーザーです")
else:
raise ValueError("ユーザーが見つかりません")
else:
raise ValueError("user_idが必要です")
# ✅ 早期リターンでガード節を使う(フラットで読みやすい)
def process_user(user_id: int) -> str:
"""ユーザーを処理する。
Args:
user_id: ユーザーID
Returns:
ユーザー名
Raises:
ValueError: user_idがNone、ユーザーが見つからない、非アクティブな場合
"""
if user_id is None:
raise ValueError("user_idが必要です")
user = get_user(user_id)
if user is None:
raise ValueError(f"ユーザーが見つかりません: user_id={user_id}")
if not user.is_active:
raise ValueError(f"非アクティブなユーザーです: user_id={user_id}")
return user.name # ここに来たら正常系だけ
💡 ガード節(Early Return)のメリット:
異常系を先に処理してreturn/raiseすることで、
以降のコードは「正常な状態が保証されている」とわかります。
ネストが深くならず、コードがフラットで読みやすくなります。
6. 🏗️ カスタム例外を作る
なぜカスタム例外が必要か
組み込みの例外(ValueError・RuntimeError)だけでは、
「なぜ・どこで起きたエラーか」が伝わりにくいことがあります。
# ✅ カスタム例外クラスの作り方(Python)
class AppError(Exception):
"""アプリケーション全体の基底例外クラス。"""
pass
class UserNotFoundError(AppError):
"""ユーザーが見つからない場合の例外。"""
def __init__(self, user_id: int) -> None:
"""初期化する。
Args:
user_id: 見つからなかったユーザーのID
"""
self.user_id = user_id
super().__init__(f"ユーザーが見つかりません: user_id={user_id}")
class InvalidEmailError(AppError):
"""メールアドレスの形式が不正な場合の例外。"""
def __init__(self, email: str) -> None:
"""初期化する。
Args:
email: 不正なメールアドレス
"""
self.email = email
super().__init__(f"メールアドレスの形式が不正です: {email}")
# 使い方
try:
user = get_user(999)
except UserNotFoundError as e:
print(f"エラー: {e}")
print(f"存在しないuser_id: {e.user_id}")
// ✅ TypeScript のカスタムエラークラス
class AppError extends Error {
constructor(message: string) {
super(message);
this.name = this.constructor.name;
}
}
class UserNotFoundError extends AppError {
readonly userId: number;
constructor(userId: number) {
super(`ユーザーが見つかりません: userId=${userId}`);
this.userId = userId;
}
}
class InvalidEmailError extends AppError {
readonly email: string;
constructor(email: string) {
super(`メールアドレスの形式が不正です: ${email}`);
this.email = email;
}
}
// 使い方
try {
const user = getUser(999);
} catch (error) {
if (error instanceof UserNotFoundError) {
console.error(`存在しないuserId: ${error.userId}`);
} else if (error instanceof InvalidEmailError) {
console.error(`不正なメール: ${error.email}`);
}
}
カスタム例外のメリット
| メリット | 説明 |
|---|---|
| 🎯 エラーの種類が明確 |
UserNotFoundError は何が起きたか一目でわかる |
| 🔍 特定の例外だけキャッチできる |
except UserNotFoundError で絞り込める |
| 📦 追加情報を持てる |
error.user_id のようにコンテキストを付けられる |
| 📖 ドキュメント化される | 例外クラスの存在自体が「このエラーが起きうる」を伝える |
7. 🛡️ 防御的プログラミング
防御的プログラミングとは?
「想定外の入力・状態が来ることを前提として、
先手を打って対処するプログラミングスタイル」です。
① 入力値は必ず検証する
def transfer_money(amount: int, from_account: str, to_account: str) -> None:
"""送金処理を行う。
Args:
amount: 送金額(円)
from_account: 送金元口座番号
to_account: 送金先口座番号
Raises:
ValueError: 入力値が不正な場合
"""
# ✅ 入力値を先に全部検証する(ガード節)
if amount <= 0:
raise ValueError(f"送金額は1円以上である必要があります: {amount}")
if amount > 1_000_000:
raise ValueError(f"1回の送金上限は100万円です: {amount}")
if not from_account or not to_account:
raise ValueError("口座番号が必要です")
if from_account == to_account:
raise ValueError("送金元と送金先が同じです")
# 検証が通ったら処理を実行
execute_transfer(amount, from_account, to_account)
② Noneを返す関数に注意する
# ⚠️ Noneを返す関数は呼び出し側に検証の責任を押しつける
def find_user(user_id: int) -> User | None:
"""ユーザーを検索する。"""
...
# 呼び出し側:Noneチェックを忘れるとエラー
user = find_user(123)
print(user.name) # user が None だとクラッシュ 😱
# ✅ 対策①:Noneチェックを徹底する
user = find_user(123)
if user is None:
raise UserNotFoundError(123)
print(user.name)
# ✅ 対策②:見つからなければ例外を投げる関数にする
def get_user(user_id: int) -> User:
"""ユーザーを取得する(見つからなければ例外)。
Args:
user_id: ユーザーID
Returns:
ユーザーオブジェクト
Raises:
UserNotFoundError: ユーザーが存在しない場合
"""
user = db.find(user_id)
if user is None:
raise UserNotFoundError(user_id)
return user
③ 型を活用して「あり得ない状態」を排除する
// ❌ 文字列で状態管理する(あり得ない値が入るリスク)
function processOrder(status: string): void {
if (status === "pending") { ... }
else if (status === "completed") { ... }
// "unknwon" のようなタイポが入っても気づかない
}
// ✅ 型で「あり得る状態」だけを表現する
type OrderStatus = "pending" | "processing" | "completed" | "cancelled";
function processOrder(status: OrderStatus): void {
// OrderStatus 以外の値はコンパイルエラーになる!
if (status === "pending") { ... }
else if (status === "completed") { ... }
}
8. 📋 エラーメッセージの書き方
良いエラーメッセージの3要素
| 要素 | 内容 | 例 |
|---|---|---|
| 🔍 何が起きたか | エラーの内容を具体的に | 「年齢の値が不正です: -5」 |
| 🎯 どの値が問題か | 問題の値を含める | 「user_id=999 のユーザーが見つかりません」 |
| 💡 どうすれば解決できるか | 対処法を示す(可能なら) | 「0以上150以下の整数を入力してください」 |
Before / After で比べる
| ❌ 悪いメッセージ | ✅ 良いメッセージ |
|---|---|
「エラー」 |
「ファイルが見つかりません: /config/app.json」 |
「失敗しました」 |
「DB接続に失敗しました。ホスト名・ポート番号を確認してください」 |
「無効な入力」 |
「メールアドレスの形式が不正です: 'alice@'(@以降のドメインが必要です)」 |
「権限がありません」 |
「管理者のみがこの操作を実行できます(現在のロール: viewer)」 |
9. 🎯 まとめ
| テーマ | 重要ポイント |
|---|---|
| 🔰 例外処理の基本 | try/except で例外をキャッチし・finally で後片付けをする |
| ❌ 悪いパターン | 例外の握りつぶし・不親切なメッセージ・広すぎるキャッチ |
| ✅ 良いパターン | 具体的な例外をキャッチ・早期リターンでフラットに書く |
| 🏗️ カスタム例外 | 意味のある例外クラスを作って情報を持たせる |
| 🛡️ 防御的プログラミング | 入力値は先に検証・Noneを安易に返さない・型で状態を表現 |
| 📋 エラーメッセージ | 何が・どの値が・どうすれば解決できるかを書く |
「良いエラーハンドリングとは、未来の自分や仲間が
深夜のインシデント対応でも困らないように書くこと」です💪
次回はシリーズ最終回!テスト・ファイル構造・バージョン管理 を解説します!
ユニットテストの書き方・ファイルの整理の仕方・コミットメッセージのルールを学びます🌟
💬 質問や感想があれば、コメント欄でお気軽にどうぞ!
👍 役に立ったら、いいね&ストックをお願いします!
🎓 ここまで読んでくださって、本当にありがとうございました!
🔗 シリーズ記事
- 【第一回】コードの基本構造
- 【第二回】命名規則
- 【第三回】コードの設計原則
- 【第四回】コメント・ドキュメント
- 【第五回】エラーハンドリング(この記事)
- 【第六回】テスト・ファイル構造・バージョン管理(近日公開)