回復可能性¶
エラーには「回復可能かどうか」の観点で2種類に分けられる
回復可能なエラー¶
次のようなエラーは回復可能なのでアプリケーション全体をクラッシュさせなくていい。
例:ユーザーの無効な入力¶
例えば、入力フォームでユーザーが正しくない形式のemailを入れた場合。
こうしたエラーはアプリケーション全体をクラッシュさせるより、入力を修正してほしい旨を伝えるエラーメッセージを出したほうがUXが良い。
例:ネットワークエラー¶
例えば、依存しているサービスに接続できない場合。
数秒待ってリトライしたり、ユーザーにネットワークを確認するよう促すメッセージを出すほうが良い。
例:深刻ではない処理のエラー¶
例えば、ソフトウェアの使用状況のログを収集する処理で起きたエラー。
ソフトウェア自体の動作を止めずに続けさせたほうがいい。
回復不可能なエラー¶
例:必要なリソースがない¶
読み込みたい画像やテキストファイルが存在せず、ソフトウェアが動作を続けられない場合。
例:コードを誤用している¶
関数の呼び出し方を間違えている
必須な事前の初期化をしていない
など
エラーから回復可能かどうかは呼び出し元が知っている¶
例えば住所をパースする処理があるとする。
def parse_address(address: str) -> Address:
if not is_valid_address(address):
... # エラーを扱うコード
...もしハードコーティングされた入力を使っていて、それが誤っている場合、回復不可能である。
def get_office_address() -> Address:
return parse_address("東京府東京市四谷区1") # 誤った住所もしユーザー入力の場合、ユーザーが修正することで回復可能である。
def get_user_address(input_address: str) -> Address:
return parse_address(input_address)エラーに対する設計方針¶
2つの方法がある
1. エラーを上位レイヤーに通知して処理を任せるか、プログラム全体をクラッシュさせる
早めに失敗させる(fail fast)ことで、問題が起きた近くの場所で通知でき、不具合の原因究明が行いやすくなる
2. エラーを処理して動作を続ける
エラーを隠す方法¶
❌️デフォルトの値を返す :例えば残高取得失敗時に0を返す → ユーザーの混乱のもとなので避けたほうが良い
❌️マジックバリューを返す :例えば-1を返す → バグの元なので避けたほうが良い
❌️何もせずreturnする :例えば書き込み失敗時にエラーを通知しない → バグの元なので避けたほうが良い
# 例:実数上で平方根を計算する関数(負の入力値はエラーとする)
def sqrt(x: float) -> float:
if x < 0:
raise ValueError("input is a negative number")
return x ** (1/2)このエラーに対応するために利用者はtry-exceptで例外をキャッチする必要がある。
def print_sqrt(x: float) -> None:
try:
print(f"{x} の平方根は {sqrt(x)} です")
except ValueError as e:
print(f"平方根の計算に失敗しました:{e}")
print_sqrt(2)
print_sqrt(-1)2 の平方根は 1.4142135623730951 です
平方根の計算に失敗しました:input is a negative number
❌️デメリット:関数が例外を返すことを利用者に伝える方法はドキュメントとなり、気づかれにくい。その点で 暗黙的なエラー通知 といえる。
null許容型 / Optional型¶
型でエラーの通知方法を示す方法。
⭕️メリット:型からエラーの存在がわかり、利用者が型チェッカーでエラーの可能性に気づくことができる。
❌️デメリット:nullだけだと「何がエラーの原因だったのか」の情報がなく、利用者はドキュメントなどを読む必要がある。
def parse_address(address: str) -> Address | None:
if not is_valid_address(address):
return None
...Result型¶
エラーについての詳細情報も含む返り値を返す方法。
Rust, Swiftなどの言語では公式にResult型をサポートしているが、他の言語でも自作のResult型を作れば同様のことはできる。
Pythonの型ヒントと共に進化するコード(#21: Result 型を自作する) が詳細な実装例を残している
# --------------------------------------------
# Result型の定義
# --------------------------------------------
from dataclasses import dataclass
from typing import final
@final
@dataclass(frozen=True, slots=True)
class Ok[T]:
"""成功を表すコンテナ
T: 成功時の値の型
"""
value: T
@final
@dataclass(frozen=True, slots=True)
class Err[E]:
"""失敗を表すコンテナ
E: エラーの型
"""
error: E
type Result[T, E] = Ok[T] | Err[E]# --------------------------------------------
# sqrt関数の定義
# --------------------------------------------
def sqrt(x: float) -> Result:
if x < 0:
return Err(error=ValueError("input is a negative number"))
return Ok(value=x ** (1/2))
def print_sqrt(x: float) -> None:
result: Result = sqrt(x)
if isinstance(result, Ok):
print(f"{x} の平方根は {result.value} です")
else:
print(f"平方根の計算に失敗しました:{result.error}")
print_sqrt(2)
print_sqrt(-1)2 の平方根は 1.4142135623730951 です
平方根の計算に失敗しました:input is a negative number
is_ok()、is_err()のような関数を作る方法もある
# --------------------------------------------
# is_ok, is_err
# --------------------------------------------
from typing_extensions import TypeIs # Python 3.12以前
# from typing import TypeIs # 3.13 以降
def is_ok[T, E](result: Result[T, E]) -> TypeIs[Ok[T]]:
"""成功かどうかを判定し、型を絞り込む"""
return isinstance(result, Ok)
def is_err[T, E](result: Result[T, E]) -> TypeIs[Err[E]]:
"""失敗かどうかを判定し、型を絞り込む"""
return isinstance(result, Err)
# --------------------------------------------
def print_sqrt(x: float) -> None:
result: Result = sqrt(x)
if is_err(result):
print(f"平方根の計算に失敗しました:{result.error}")
return
print(f"{x} の平方根は {result.value} です")
print_sqrt(2)
print_sqrt(-1)2 の平方根は 1.4142135623730951 です
平方根の計算に失敗しました:input is a negative number