エラー指針 2025
#エラー #俺の思考 #例外処理
関連
例外処理 指針
例外処理 指針 2024版
過去に指針とか作っておきながら全く身についてないので、懲りずにもう一度整理する
もっとinputしよう。ただ、inputの前に今の自分の純粋なエラー指針を頭から吐き出して整理する。その後inputする。
とりあえずのMy理論として据え置き、常に改善を怠らない姿勢。(1ヶ月後には忘れてそう)
TODO一覧
todo.icon自身も理論をもとに様々な有名記事や開発者の発言を読んでブラッシュアップ
todo.icon 以下続きを読みなさい
https://lpalmieri.com/posts/error-handling-rust/#errors-for-control-flow
todo.icon理論を図解したい...(3メンバーの再帰という図よって理論を説明できる気がする)
todo.icon記事に関して
記事を書くときは「エラーを求める者は誰か」「システムの単純化」という説明を最初にしてあげないといけない
そして、それぞれの求める者たちにどうアプローチしていくかを書けばわかりやすいと思う。
記事にする場合は、「仮説である」と強調しFBを求める姿勢でいきたい
FBを求めるし、なんならいろんな人たちの理論を知りたい
究極的には、エラー設計本を誰か出して欲しい
現時点のMy理論(とりあえず自分の道標とする)
hr.icon
なお、以下理論はRustとかGo周りのreturn error文化に寄ってると思う。
基本ルール
1. モジュールは「自身が返す可能性のあるエラー」を仕様として定義すべきである
2. 定義するエラーは、モジュール内の具体知識を持ってはいけない
「具体知識」= 関数シグネチャ(引数・戻り値の型含む)から推論できない実装選択
判断基準: その失敗モードは、どんな実装でも起こりうるか?
NO → 実装依存の具体知識 → 名前に含めない
ex) saveData(db_client: DatabaseClient, data: Data)
→ DatabaseConnectionError はOK(DatabaseClientはシグネチャの一部)
→ MySQLConnectionError はNG(MySQLはシグネチャから見えない実装)
必要のない依存が出来上がってしまう。エラーで密結合してしまう。
3. モジュールの使用者が回復できる可能性のあるエラーなら名前をつけて返してあげる
以下に何が名前をつけるエラーなのか、具体例を参考に思考していく。
①: 契約に反した、仕様の取り決めに反した引数が与えられる
いわゆるInvalidArgumentError。
使用者が事前条件に反した引数を渡してるので、使用者が回復できる。
なのでモジュールが返す可能性のあるエラーとして名前をつけて、モジュールの仕様にする
これは一番わかりやすいタイプのエラーで、機械的にできる。
②: readJson(file_path: string)関数
使用者が直接与えた情報に関するエラー(名前をつける):
InvalidPathError - file_pathが空または不正な形式
使用者が知っている範囲の外部リソース操作エラー(抽象化して名前をつける):
InvalidJsonError - JSON形式が不正
file_pathを渡している時点で、その中身についても知っているはず
使用者はファイルの内容を修正できる
ResourceNotFoundError - ファイルが見つからない
file_pathを渡している時点で、外部リソースへのアクセスがあることは予測している
使用者は正しいパスを指定し直すことで回復できる
AccessDeniedError - リソースへのアクセスが拒否された
同様に、外部リソース操作には アクセス制御の可能性があることを想定しているはず
使用者は権限を確認・修正することで回復できる可能性がある
③: sendEmail(to: string, body: string, config?: EmailConfig)関数
使用者が直接与えた情報に関するエラー(名前をつける):
InvalidEmailAddressError - toのメールアドレス形式が不正
InvalidEmailBodyError - bodyが空または許容サイズ超過
InvalidConfigurationError - configの形式や値が不正
AuthenticationError - config内の認証情報が不正(configを渡している時点で認証が必要と知っている)
④: saveData(db_client: DatabaseClient, data: Data)関数
使用者が直接与えた情報に関するエラー(名前をつける):
InvalidDataError - dataが契約違反(必須フィールド欠如、型違反など)
InvalidClientError - db_clientが未初期化または不正な状態
DuplicateKeyError / UniqueConstraintViolationError - ユニーク制約違反
使用者がdataを与えており、その内容が既存データと重複している
使用者は重複を避けるようにデータを修正できる
使用者が知っている範囲のDB操作エラー(抽象化して名前をつける):
DatabaseConnectionError - DB接続失敗
db_clientを渡している時点で、DB操作があることは予測している
リトライや別のクライアント利用などで回復できる可能性がある
DatabaseOperationError - DB操作失敗(クエリタイムアウト、トランザクション失敗など)
同様に、使用者はDB操作の失敗可能性を想定しているはず
重要:
❌ MySQLConnectionError、PostgreSQLErrorのような具体的なDB名は含めない
⭕ DatabaseConnectionErrorのような抽象化したエラー名にする
使用者は「DBを使っている」ことは知っているが、「どのDBか」は知らない(知るべきでない)
補足: 解釈の違いについて
設計者によって解釈が分かれるかもしれない
こっちの方がいいって意見もあるかも。自分もまだまだ未熟なので。
でもいずれにせよ「使用者の責任範囲内で解決できそうか」「使用者が入力や振る舞いを変えることで対処できそうか」
という視点を持って、指定モジュールのエラー仕様を決めると良いと思う。
4. 使用者が具体知識を持たないと解決できないエラーには名前をつけない
前提として、使用者は具体を知ってはいけない(原則2)。
なので、使用者は具体的知識がないと解決できないエラーを解決できない。
だから、仕様で名前をつけても意味がない(知っても何もできないから)
これを私は「システム開発者・運用者が対処するエラー」と定める。
5. 使用者が直接回復できないが「リトライで解決する可能性がある」エラー
具体的な原因を名前にせず、回復戦略を属性で伝えることで使用者に情報を提供する
例: sendEmail(to: string, body: string, config?: EmailConfig)の場合
❌ RateLimitExceededError - レート制限は実装依存の具体知識(原則2違反)
❌ NetworkTimeoutError - ネットワーク層の詳細は実装依存(原則2違反)
⭕ EmailSendError { retryable: true, retryAfter?: 60 } - 抽象的なエラー名 + 回復戦略の属性
理由:
具体的な原因(レート制限、ネットワーク障害等)は「原則2」により実装依存なので名前に含めてはいけない
本来は「原則4」により名前をつけるべきでないエラー(開発者依存のエラー)
しかし、「リトライすれば解決するかも」という情報は使用者が回復するために必要
属性で伝えることで、実装詳細を隠蔽しつつ回復戦略を提供できる
アプリなどでもたまに「再起動してみてください。治るかも」ってあるでしょう。
あれと一緒です。
6. なお、上記「3.」「4.」「5.」のエラーの区分けに答えはない(と思う)。
設計の状況や解釈によって変わるかもしれない。
自分の場合は、「関数シグネチャから、使用者が知ってそうかを判断するのが丸い」と思っている。
「暗黙的に知っている」という設計は避けたいと思っている。
が、まぁ人次第かなって感じ。onigiri.w2.iconはそう思っているってだけ。
けど、「エラーに具体知識を含まない」という原則は守った方がいいと思う。上手く言えないけど。
7. 最後重要:可能性あるからといって、無闇矢鱈にエラーを定義しないこと。必要になってから定義するとよい。
必要になるまで定義を遅らせることで、無駄なエラー定義を避けられる。
モジュール仕様としては正しいエラー定義でも、実際に誰も使ってないなら無駄に複雑性が増えるだけ。
「3.-②」を例に挙げると...
ある使用者の1つから「読み込み権限がないことに対して回復したい」という要望が出るまでは、定義しないでおく。
YAGNIってやつ。
8. エラーオブジェクトには以下あたりをデバッグ情報として詰めておくと良い
とりあえず以下あたり?
1. コンテキスト(エラー発生時に関わったデータ。引数以外もあるなら入れる)
2. エラーの原因となるエラー(存在するなら)
この情報が開発者に伝わって解決の役に立つ。
この情報は、publicで置いておきたくない。使用者に使われる可能性が残るから。
ただ...今のところ技術的にうまくやる方法を知らない。
_debugっていうプロパティに詰めるとかそういうことしかできないかも。
完全でスマートなカプセル化方法が今の自分にはわからない。
とはいえ、開発者による解決のために必要なので入れざるおえない。
エラー設計には「モジュール利用者」と「開発者」の2つのアクターが存在する と思っている。
課題
開発者しか直せないエラーってわかった時点で、可能な言語ならpanic!させる?
そうしない理由が自分の中にあるはず...整理できてない...
「シグネチャ」「仕様」から対処できそうなエラーを推論、予測するとあるが、もっとハッキリした線引き・基準はないのか。ルールが曖昧でうまくできるかわからない。
それな。俺もまだわかりません。答えを知ってる人教えてください。
この理論は、なんか他の設計原則の理解も前提に話している気がするが、自分は直感で書いてるので細かく洗い出せてない。よく言われてる理論とか前提になってると思う。
なんか「冗長」で「オーバーエンジニアリング」にならないかな?
まぁそれはありえる。でも、理想を知っておかないと妥協が妥当かわからなくね?
比較して判断できない気がするんだよなぁ。でも、自分自身妥協の仕方をまだ知らない。
どうやればバランス取れるんだろう。
例えば、contextを全て取る必要ないとか?抽象化を毎回するなとかいう批判あるのかな?
今回の指針は並行プログラミングを考慮してない。なのでそこに通用するかは不明
どうなんだろうな。わからん。知らん。
性能問題とかの現実的なところは考慮してない。それもわからない。
モジュール境界について自分もまだ咀嚼しきれてないからしたい。その前提が必要そう
密結合・知識を共有する境界を都度決めていく的なイメージしてる。
つまり、全てを疎結合にするのではなく、一定範囲で区切っていく。
一定の範囲で再利用可能な単位をモジュールとして的なイメージ。
ここら辺を理論的に書いてる名記事があったはず(Qiitaに)
題名を忘れた...orz
何度も同じエラーを変換する可能性があることに懸念。
例えば、同じPermissionErrorなのにその層の名前で定義し直すみたいな。
解釈の指針がまだまだ曖昧だ
リスコフの置換原則をエラー設計にも応用しろといえばいいだろうか。
「その返すエラーは、中身がどんな具体だとしても違和感ないか」を基準にするか?
並行を考慮してないのと同様、「回復処理中の失敗」「回復後の失敗」の際のエラーの扱い方、debug情報への詰め方などは考慮してないな
単純に単純に考えるとエラークラスを変更することで対応するかも
前提イメージ
https://scrapbox.io/files/692d5f8ea5f09fe5acf12ac4.png
https://scrapbox.io/files/692d61d99912f165fdbf7557.png
https://scrapbox.io/files/692d631b21df065438c66623.png
throwに対して思うこと
hr.icon
throwでエラー発生箇所のコンテキストはわかるけど、entrypointから発生場所の間のコンテキストが抜け落ちるんじゃね?
例えば「main(args) -> sub(args) -> third(args) -> forth(args)」とある
forth(args)のargsに不正文字が入ったとする。
すると、forth(args)は、エラー発生時の自身のコンテキスト(引数など)をエラーに詰めて上に投げる。
大抵は、main(args)がこれを直接キャッチして、自身のコンテキストと一緒にエラーとstacktraceをロギングする。
この時、中間の経路はstacktraceからわかるけど、そのコンテキストはわからない。
そして、main(args)が犯人じゃない場合、中間ルーチンのどれかになるのだが...
どいつがどんな値を渡したのかはわからない...
エラーの原因調査に時間かかることになる。
でも、そこまで神経質にならなくていいのか?わからん。実務的に問題ないのかもしれない。自分は経験してないから机上の空論説ある。理想論説ある。誰か何か教えてください。
思考垂れ流し
hr.icon
エラーをシステム全体で考えずに、1つの部品におけるエラーを考えることえ設計すべし
エラーはそもそも部品・モジュールが返す可能性のある値、戻り値と考えるべし
エラーは戻り値である。モジュールとは別で考えるものではない。
そのモジュールこそが、返すエラーを定義して仕様として外に伝えるべき。
エラーが、利用「する側」「される側」のどちらに責任があるのかを明確にすると扱いやすくなる
「する側」に責任があるなら、対応策が取れるようエラーに名前をつけ、原因などもわかるように返す
「される側」に責任があるなら、「する側」に対応策がないので、詳細を伝えなくてもいいしエラーも分けない。
q.icon ここが少し自分でも違和感ある。本当にエラー名は同じでいいのかな?
まぁエラー名は仕様で定義しないって感じ。使用者に伝えても役に立たないなら名前をつける意味はない。
と考えている。
内部のエラーを外にそのまま返すのは良くないと思う。密結合を発生する。
最初に言ったように、エラーはモジュールの仕様。
エラーがそのまま伝播するのは、内部の仕様が外部に漏れ出ていくことと同じだと思う。
q.icon エラーオブジェクトに、内部処理のエラーチェーンで溜まったcauseなどを持たせて、使う側に返すのは良いのか?
カプセル化、IFの重要性などを考えた際に、内部の内容が外に漏れてないだろうか
a.icon 開発者がエラー調査をする際に必要になる。
ただ、カプセル化的にはprivateとかにおいておきたい。
でもそうすると、開発者がその情報を使えないことも意味するので、どうしよう...って感じ...
まぁ、_debugとかそういうプロパティの中に詰めるとかいう弱めの回避策くらいしか思いつかない。今。
「回復可能エラー」 か 「回復不可能なエラー」 かで分ける方法もある。
これはrustやgoの考え方だと思う。不能なものは panic!にしろっていうね。
でも、ネットワークエラーはどっちになる?どのモジュールにおいても、選択の余地がありそうなんだが。
a.icon そうそうpanic!する必要はないと思う。
再コンパイルしないと治りませんと、その場で判断できるなら良いと思うけど。..
箱庭理論:「使う側」「使われる側」「環境」
環境が原因の場合、どっち使う/使われるどちらの責任でもない気がする。
環境が原因でネットワークエラーが起きた場合、それはどっちの責任と言える?
でも、少なくともどちらの責任とも言えないとは言えるか。MECEに考えると。
そもそも、事前条件エラー以外のエラーを、「使う側が回復判断すること」は可能なのか?
もし回復判断できる場合、それは具体情報に依存してるんじゃないだろうか。
例えば、DB接続が失敗しました、それはdb_pathの先にdbが存在しないからです。db_pathは使われる側でハードコードしてます。
これは、まず責任はどこにあるのかというと、不明。少なくとも「使う側」ではない。
「使われる側」か「環境」のどちらか。
で「使う側」が原因ということはないはず。使う側は与える引数を正す責務があるのははっきりしてるが。
やはり、具体エラーの情報をもとに上位が回復を行うというのは違和感がある。
それをやるよりは、モジュールがエラーを定義してそのエラーを返し、そのエラーの情報で回復できるならどうぞって感じにしたい。
事前条件エラー以外は、基本的にすぐ回復できないエラーになる。
Input(inputをしつつ、自分の理論との差分を確認したり、少し批判的に論理的に読む)
hr.icon
https://nick.groenen.me/posts/rust-error-handling/
I think the bit about error handling being different depending on if you’re writing a library vs an application is simplification that’s common in the rust community but also a source of confusion.
The reasons for using anyhow vs thiserror aren’t really based on if it’s a library or an application, it’s actually about whether or not you need to handle errors or report them.
Libraries often want to support as many error handling use cases for their consumers as possible. This ends up meaning that they want to export error types that are both handleable (aka an enum) and reportable (aka implements std::error::Error).
Applications on the other hand often end up doing the error handling or reporting. For handling you don’t need a library usually, you just use match. For reporting you do need an error type, or more accurately an error reporting type, which is exactly what anyhow::Error is designed to do.
これはonigiri.w2.iconの感じてることと似てると思う。
エラーというのは「使う側がハンドリングしたい」時のみ初めて名前をつける。
使う側がこういうエラーが発生したのなら教えて欲しいと要望が出ない限り、エラーに名前をつけなくても良いという方針。
「使う側」が必要ないのなら、以降も必要とされないので、開発者が対応する障害として扱うべし。
https://nrc.github.io/error-docs/error-design/thinking-about-errors.html
Can a program recover when an error occurs? Documentation for Rust often starts with a split between recoverable and unrecoverable errors. However, I think this is a more complicated question. Whether a program can recover from an error depends on what we mean by recovering (and what we mean by 'program' - is it recovery if an observer process restarts the erroring process?), it may depend on the program state before and after the error, it may depend on where we try to recover (it might be impossible to recover where the error occurs, but possible further up the call stack, or vice versa), it may depend on what context is available with the error. Even if it is technically possible to recover, if that would take an unreasonable amount of effort or risk, then it might be better to consider the error as unrecoverable.
There is also the question of intention. An error might be technically recoverable, but a library might want to treat it as unrecoverable for some reason.
So, I think there is a whole spectrum of recoverability which we can consider for errors and the context where the error occurs.
まさにonigiri.w2.icon
そのモジュールだけで「回復可能」か否かってわからん。
ネットワークエラー1つとっても、モジュールのみの視点からだと回復可能っぽく見えても、システム全体の状況から見ると不可能だったりする。
そのモジュールだけで判断することができない。
だから、「判断として必要であればエラーとして返すよ」くらいのスタンスでいいと思う。
必要とされるまでは、全て開発者が解決するエラーとしておくからね。と。
まぁ、アプリケーション系だとこれでいいが、ライブラリとかだと最初から要望を予測して名前をつけて返す必要が出てくるかも。
https://dave.cheney.net/2016/04/27/dont-just-check-errors-handle-them-gracefully
errを無理にキャッチせず、そのまま上に返そうとしろ
でも、それに忠実すぎると、最終的にどこでエラーが起きたのかわからない
だから、戻ってきたerrに対してコンテキスト付与しながら上に伝播していけ
的なこと言ってますonigiri.w2.icon
これには賛成します。
抽象的に設計がどうのこうのって話は置いておいても、エラー対処する開発者からしたら、コンテキストが繋がってないと訳わからないからな。
In operation, whenever you need to check an error matches a specific value or type, you should first recover the original error using the errors.Cause function.
結合度を下げたい委員会所属の自分としては、これは簡単に受け入れられないonigiri.w2.icon
Causeで、エラーの原因がわかるというのは、1つ下の具体を知れるということ
システムの最上位層からより深い層への依存が起き得ることになる
これ許容していいのか?現実的に仕方ないのか?意味的には自然なのかな?
errを返す際に同時にloggingもするな。的なこと言ってるonigiri.w2.icon
これは同意しますonigiri.w2.icon
基本的には、エラーloggngは最上位で行うのがわかりやすいと思っている。
ただ、例外やら設計上の戦略の違いとかはあるかも。
システム境界でloggingするとか。まぁそういうのはあるかも。知らんけど。
https://solidabstractions.com/2019/error-handling-introduction
https://solidabstractions.com/2019/error-handling-levels
https://solidabstractions.com/2019/error-handling-techniques
自分の思想に非常に似ている。githubやblogを見ると、彼はもうエンジニアを続けているのか怪しいが、それでも自分の思っていることと通ずる人がいたことに少し安心したonigiri.w2.icon
「エラーも契約の1部なんだ」という前提をおけば、抽象化を守ることに合点がいくと思う。
https://lpalmieri.com/posts/error-handling-rust/
What Is The Purpose Of Errors?
1. Enable The Caller To React
The caller of execute most likely wants to be informed if a failure occurs - they need to react accordingly, e.g. retry the query or propagate the failure upstream using ?, as in our example.
2. Help An Operator To Troubleshoot
What if an operation has a single failure mode - should we just use () as error type?
Err(()) might be enough for the caller to determine what to do - e.g. return a 500 Internal Server Error to the user.
But control flow is not the only purpose of errors in an application.
We expect errors to carry enough context about the failure to produce a report for an operator (e.g. the developer) that contains enough details to go and troubleshoot the issue.
What do we mean by report?
In a backend API like ours it will usually be a log event.
In a CLI it could be an error message shown in the terminal when a --verbose flag is used.
3. Help A User To Troubleshoot
(長すぎるので要約してますonigiri.w2.icon)
Users, like operators, expect the API to signal failures.
For internal errors (500): Empty body is correct - users don't need API internals.
For validation errors (400): Currently returns empty body. This is poor UX -
users are left in the dark about what went wrong (invalid email? invalid name? why?).
They cannot adapt their behavior without this information.
The error message exists internally ("X is not a valid subscriber email")
but is discarded before reaching the user.
この本は名著の匂いがするぜ。Rustをより使いこなしたい場合にここにまた戻ってこようと思うonigiri.w2.icon
いずれにせよ、この人が言ってる視点は自分のものと基本的に似ていると思う。
誰のためのエラーなのか、それは「モジュールを呼ぶ者」と「運用者・開発者」の主に2人いる。
そして、追加でエンドユーザーも見てるって感じ。
onigiri.w2.icon はエンドユーザーも「モジュールを呼ぶ者」と捉えれると思っていたので、この視点を入れてなかった。
なんなら、CLI ツールのユーザーとかの場合は、エンドユーザーだけど詳しい情報が欲しいみたいなこともあり得ると思う。
詰まることろ、「受け取り手の要求」によって変わるということは注意しておきたい。
少しモチベ下がったけど読みたいから続きは以下から
https://lpalmieri.com/posts/error-handling-rust/#errors-for-control-flow
https://www.reddit.com/r/learnpython/comments/yxcqib/exception_handling_best_practices/
関数が発生させる可能性のある例外をすべてどうやって知るのか
必要ありません。エラー処理は、既知/一般的なケースを「処理」し、未知/エッジケースを「ノイズ」として扱うようにアプローチする必要があります。
一般的に、既知/一般的なケースはシステムが継続して動作するように穏やかに処理し、未知/エッジ ケースではシステムが急速に激しく障害を起こすようにします。
はい。これが全てですonigiri.w2.icon
最初なんか全部障害扱いでいいとすら思っている。面倒なら。