どうもエラーを出すもしくはエラーが出るのが怖いという人がいるみたい。例えば改修を行うときに既存部分でエラーが出ないことを最優先にして増築を行いいびつな構造を生み出すとか、単純に例外を全然使わないとか。エラーが出ると、「うわ、エラーになった。手間かけさせやがって面倒だなぁ…」みたいな感覚があって、とにかく自分がコードを書くときも一切例外を投げないというスタンスをとりがちなのかもしれない。
私はここで、適切にエラーが出てくれるのはむしろ喜ばしいことであり、自分がコードを書くときも積極的にエラーを出すようにすべきだ、という主張をする。
関数定義のドキュメンテーションの一部
ある関数の中身で一番最初に書くべき処理は何か、それは引数のチェックをして条件を満たさなければエラーを出すことである。例えば文字列は特定の形式になってなければならないとか、数値に最大値最小値があるとか、これらは関数の入力の前提条件としてよくあることだが、それをちゃんとチェックする。
function f(s: string, o: { isHoge: boolean, fuga?: number }) {
if(!/A-\d{4,8}/.test(s)) {
throw new Error('"s" の形式が不正です');
}
if(o.isHoge && o.fuga == null) {
throw new Error('"o.isHoge" が真の場合は "o.fuga" は必須です');
}
// 処理本体が続く…
}
引数の前提条件は余すところなく関数冒頭で表現しなければならない。型で表現できることは引数の型で、型で表現できないことはチェック処理を書く。こうすることで関数の役割および使い方が明確になる。関数定義の上の方を読むだけで、この関数は入力としてどんなデータを取るのかが理解できるようになっているべきである。
この関数を使うとき、引数の型が間違っていればコンパイル時にエラーになり、型で表現できない詳細な前提条件が間違っているときは、コードの実行時にその旨が正しくエラーとして返ってくる。チェック処理で適切なエラーを返すことによって、コンパイル時には劣るが、高確率でミスに気づくことができる。
本当に怖いのはエラーが出ることではない、エラーが出ないまま実はおかしな挙動になっている場合である
適切なエラーが出てくれるいうのは実に素晴らしいことだ。なぜならエラーが出てくれればミスに気づけるからである。エラーが出ると処理が完全に中断される、中断されれば何かがおかしいという事は嫌でも気づける。
本当に怖いのはエラーが出ないまま、実は想定外のおかしな処理をしている時である。この状態では実はバグが含まれているということに気づけないまま時間が経過し、後になって重大な影響を及ぼすということが起こりえる。
例えば以下のコード、
function paymentMethodLabel(paymentMethod) {
if(paymentMethod === 'creditCard') {
return 'クレジット―カード払い';
} else {
return '口座振替';
}
}
ここに第3の決済方法「コンビニ支払い」を追加する機能改修が入ったとする。これは大きな改修なのでコードベースの様々な部分で変更が必要になる中、このpaymentMethodLabel
の改修を忘れてしまった!この関数はコンビニ支払いの場合でも「口座振替」が返り、気づかずにそのままリリースしてしまったという事もあり得るだろう。
どうすれば防げたか、ちゃんとエラーを投げていればよかった。
function paymentMethodLabel(paymentMethod) {
if(paymentMethod === 'creditCard') {
return 'クレジット―カード払い';
} else if(paymentMethod === 'directDebit') {
return '口座振替';
} else {
throw new TypeError('paymentMethod must be "creditCard" or "directDebit"');
}
}
最初からこのように書いておけばpaymentMethod
に第三の値が来た場合に即座にエラーが投げられ処理が停止する。そうすればこの関数の改修を忘れていることに自然と気づけるだろう。
バグに気づけないという最悪の状況に比べればエラーが出るというのはその何倍もましである。エラーを投げるというのは気づけないバグを減らすという観点からとても重要なことである。
関数型プログラミングに関する補足
関数型プログラミングの考え方(特に直和型)を使うと、例外のような実行時エラーをコンパイル時エラーにすることができる場面が多い。当該部分のコードが実行されるされないに関わらずコンパイル時にエラーが出るのでより安心安全になる。
// 引数 o を直和型を使った表現に変更
function f(s: string, o: { isHoge: true, fuga: number } | { isHoge: false }) {
// 以下の実行時チェックが不要
// if(o.isHoge && o.fuga == null) {
// throw new Error('"o.isHoge" が真の場合は "o.fuga" は必須です');
// }
// 処理本体が続く…
}
f('A-0000', { isHoge: true }); // コンパイル時エラー!
// 引数を直和型で表現
function paymentMethodLabel(paymentMethod: 'creditCard' | 'directDebit') {
if(paymentMethod === 'creditCard') {
return 'クレジット―カード払い';
} else if(paymentMethod === 'directDebit') {
return '口座振替';
} else {
throw new TypeError('paymentMethod must be "creditCard" or "directDebit"');
}
}
paymentMethodLabel('convenienceStore'); // コンパイル時エラー!