エラーメッセージの書き方
この記事はしおだいふく Advent Calendar 2020 8日目の記事です。
エラーメッセージを上手に書けるようになるための入門編です。 この記事で取り上げた例は多様なエラーメッセージのほんの一部に過ぎませんし、 メッセージタイプの分類もすごく雑です。 あくまで考え方のとっかかりとして捉えていただければと思います。
内部処理のエラーメッセージ
まずは内部処理のエラーメッセージに関して見ていきましょう。
ここでいう内部処理とは、汎用的な関数やライブラリが提供する機能を指します。 メッセージの読み手はその内部処理を利用してなんらかの機能を開発している最中のエンジニアを想定しています。 内部処理が発生させる例外はアプリケーションの実行時には発生しないか、発生する場合は適切にハンドリングされることが期待されるものです。
この種類のエラーメッセージは開発者が開発中に読むものなので、通常それほど差し迫った状況ではありません。 今まさに開発しているコードなので、ある程度周辺の処理内容に詳しいことも期待できます。
エラーの発生箇所が特定できる
内部処理のエラーメッセージはエラーの発生箇所が特定できることが重要になります。 利用するプログラミング言語によっては生成されるスタックトレースがこの要請を満たす場合があります。
基本的なことではありますが、例えばエラーを発生させた関数名/メソッド名をメッセージに含めるといった作戦が有効です。 非同期処理など単純に関数名だけでは情報が不十分なケースも考えられますので、関数に与えられた引数などエラーが発生した状況を知る手がかりとなる情報を詰め込むとなお良いでしょう。
また、1つの関数の中に複数エラーが発生しうる箇所がある場合は、どこでエラーが発生したのか特定できるような粒度の細かいハンドリングをすべきです。 次のイマイチな例は、割とよくやりがちなエラーハンドリングです。
// イマイチな例
const doSomething = async (input: string) => {
try {
await importantBusinessLogicA(input)
await importantBusinessLogicB()
} catch(error) {
throw new Error(`doSomething failed: ${error.message}`)
}
}
やや手間ではありますが、次のようにtry-catchを分割することでAが失敗したのかBが失敗したのかを切り分ける手間が省けます。
// マシな例
const doSomething = async (input: string) => {
try {
await importantBusinessLogicA(input)
} catch(error) {
throw new Error(`importantBusinessLogicA in doSomething failed: ${error.message}`)
}
try {
await importantBusinessLogicB()
} catch(error) {
throw new Error(`importantBusinessLogicB in doSomething failed: ${error.message}`)
}
}
エラーの原因がわかる
もうひとつのポイントはエラーの発生原因がわかることです。 エラーが発生しなくなるためには、何を直せばいいのかをエンジニアに伝えることができるとよいでしょう。
パラメータのフォーマットが誤っているだとか、関数を呼び出すための事前条件が満たされていないだとか、 そういった直すべきポイントを教えてあげると親切です。
// マシな例
const importantBusinessLogicA = (input: string) => {
return new Promise((resolve, reject) => {
if (input.length === 0) {
reject(new Error('parameter input should not be empty string'))
}
// ......
})
}
システムアラートの元になるエラーメッセージ
多くのシステム運用の現場では、エラーログを監視するツールを利用して障害の発生を検知する仕組みが整備されています。 一般的に、ツールによる監視の対象となっているメッセージはそのままメールやSlackなどで送信され、障害対応の最初の手がかりとなります。
障害発生中はかなり緊迫している場面であり、対応にあたっているエンジニアは(通常)心の余裕が失われているにも関わらず、 速やかに何が起こっているかを理解しなければならないという強いプレッシャーがかかる状況です。 これに加えて、障害対応は必ずしもそのコードを書いた人が対応するわけではありません。 当番制で全く内容を知らない人がアラートを受信することもあるでしょう。
先述した内部処理のエラーメッセージを読むケースとは状況が大きく異なります。 そういった場面を想定した上で、有益なエラーメッセージのポイントを挙げていきます。
ユーザへの影響がわかる
障害対応の初期に必要になるのは、実際にユーザに対してどのような問題が起こっているかを特定することです。 これがアラートのメッセージとして飛んでくるだけで影響調査の手間が省けます。
try {
await getGraphData()
} catch(error) {
console.log(error.message)
throw new Error('ダッシュボードの〇〇グラフが表示できなくなっています。')
}
初動対応で何をすべきかわかる
次いで重要なのが、そのエラーが起こったときにどのような初動対応を取るべきかを出力することです。 長くなる場合は、参照すべきドキュメントのURLを書いておくと親切かもしれません。
try {
await getGraphData()
} catch(error) {
console.log(error.message)
throw new Error('ダッシュボードの〇〇グラフが表示できなくなっています。対応手順: https://faq.on.error.com/sample')
}
この飛んだ先の手順が複雑だとそれはそれでしんどいのですが、少なくともなにもないよりは100倍マシです。
機密情報を書かない
これは障害発生時の情報源として何でもデータをログに書きまくったりしているとうっかりハマりがちな罠です。 機密情報(や個人情報)は安易にメッセージに含めてはいけません。 必要な場合は、エラー監視ツールによってアラートが飛ばないようなログレベルで必要最小限の情報をログに残すようにしてください。
障害アラートは誰が受信しているかわかりませんので、油断すると情報漏洩事故を併発します。 業種や書いてしまった内容によってはログファイルを懇切丁寧に扱わなければなくなったりします。
読み手を意識して適切なメッセージを!
例えば、障害アラートとして「関数hogehogeの実行に失敗しました」とかいうメッセージが飛んできてもあまり嬉しくありません。 そこからコードを読んで、何が起こってるかを推測して、裏取りをする、みたいな作業を障害対応中にやるのはサービスの規模が大きくなればなるほど精神に与えるダメージが大きくなります。 良いエラーメッセージの条件は、それらのステップをすべて省略できるだけの情報を持っていることといえます。
同じエラーメッセージでも状況が違えば役に立たないことはよくあります。 例外処理系のメッセージは適当に済ませずに、読み手が誰か、そのメッセージをどういう状況で読むかに常に思いを馳せることが大切です。 そのようにして書かれたメッセージは必ず多くのエンジニア、ひいてはプロダクトのステークホルダに幸せをもたらすでしょう。
マジで頼むぞ。