JavaScriptのような動的型付け言語でプログラミングする際、例外処理というのは特に重要です。
なぜ重要なのか、以下の例題を通じてお伝えしたいと思います。
異常終了を避ける試み
ブラウザはJavaScriptの実行を試み、それがエラーだった際は動作を止めてしまいます。
これを異常終了と言います。
異常終了したページは動作を受け付けません。リロードするか別のページに遷移するか、いずれにしても想定外の動作をユーザーに強いることになります。
だからアプリ側でエラーが出た際は前もって準備した動作を用意しておいて、異常終了を回避します。
また、ブラウザ上でユーザーに対して明示的にエラーした旨を伝える必要があります。
これを例外処理といい、エラーハンドリングとも言います。
例外処理の基本的な流れ
ポイントは、失敗(=エラー)の際、通常処理を行わないという点です。
また、失敗しても異常終了でなく正常終了するという点です。
この大まかな流れをイメージして、簡単なアプリを一から作っていきます。
例えば今週のスプリントのひとり当たりSPを計算する
消化したいSPが80として、開発者が8人だとしたら一人当たりSPは10。
ブラウザ上で入力していって、最終的にalertで表示したい。
最終的な出力を考えてみる
alert(`一人当たりのタスク数は${result}です`);
最終的な結果をresultとして、テンプレートリテラルの中で表示する。
これをゴールとします。
resultをどう計算するか
resultを求めるためには、総SPを総人数で割ればよい。
割り算する関数share()を後で作成するとして、resultはこのように代入できる。
const result = share(inputPerson, inputSP);
share関数を作成する
総SPを総人数で割り算する関数、そんなの簡単!と思って↓こういうのを作ってしまう。
function share(inputPerson, inputSP) {
return inputSP / inputPerson
}
これには少なくとも2つの問題があります。
0で除算されてしまう問題。
何かをゼロで割り算するということは数学的には未定義です。
つまりアプリとしてはエラーということ。
(JSではエラーとならずInfinityが返る)
ちなみに、ゼロ除算の定義可能性は数学上のとても興味深い命題です。
解決策:0以下のチェックを入れる
function share(inputPerson, inputSP) {
if (inputPerson <= 0 || inputSP <= 0) {
alert('正の整数値で入力してください')
}
return inputSP / inputPerson
}
まだ完全ではありません。
文字列を入力されたときの問題
数値でなく'あいう'、'ABC'などを入力された場合、もちろん割り算ができません。
他のプログラミング言語ではエラーとなりますが、JSではエラーではなく
NaN (Not a Number)
が返ってきます。
解決策:数値型のチェックを入れる
function share(inputPerson, inputSP) {
...
const person = parseInt(inputPerson);
const sp = parseInt(inputSP);
if (!Number.isInteger(person) || !Number.isInteger(sp)) {
alert('入力値が不正です');
}
...
return inputSP / inputPerson
}
parseInt関数は、引数を整数値に変換します。変換できない場合はNaNを返します。
(実際の動作はもっと難しいが割愛)
!Number.isInteger(person)
personが数値型であることを確認して、falseであれば、「入力値が不正です」とalertします。
この部分は少し冗長ですがこのまま進みます。
promptを表示する
promptはブラウザでポップアップウインドウとして表示できる「キャンセル or OK」のボックスです。
Window.prompt()
Windowオブジェクトです。
頭のWindow.は省略できます。
prompt('スプリントのSPを入力してください');
宣言するだけですぐに実行されます。
const prompt('スプリントのSPを入力してください');
代入と同時に実行もできます。
フロントエンドで処理の流れを試験的に検討したいというシーンで、いちいちコンポーネントのファイルを作ってルーティングして...などとやっていると手間がかかりすぎます。
promptを利用してとりあえず動作確認してみる、というのは効率的な手法です。
promptの順を考える
コードにするとこうなります。
const inputSP = prompt('スプリントのSPを入力してください', 80);
const inputPerson = prompt('スプリントのタスクを分ける人数を入力してください', 8);
const result = share(inputPerson, inputSP);
alert(`一人当たりのタスク数は${result}です`);
promptの第二引数は初期値です。
指定した値が初めから入力されている状態になります。
これで完成...?
function share(inputPerson, inputSP) {
if (inputPerson <= 0 || inputSP <= 0) {
alert('正の整数値で入力してください')
}
const person = parseInt(inputPerson);
const sp = parseInt(inputSP);
if (!Number.isInteger(person) || !Number.isInteger(sp)) {
alert('入力値が不正です');
}
return inputSP / inputPerson
}
const inputSP = prompt('スプリントのSPを入力してください', 80);
const inputPerson = prompt('スプリントのタスクを分ける人数を入力してください', 8);
const result = share(inputPerson, inputSP);
alert(`一人当たりのタスク数は${result}です`);
処理そのものは完成しました。
これで1以上の整数値を入力すれば正常動作します。
0以下の数値や文字列が入力されてもアラートを表示するようにしています。
しかし、0以下の数値や文字列が入力されると、resultを表示する最後の関数が実行されてしまいます。
さらに、その値はNaNやundefindとなり、想定していない値が変数に代入されてしまいます。
実際のアプリでは、データベースに保存するなどの関数が動いて、NaNやundefindを保存しようとします。
データベースは、あらかじめ保存できる型や桁数を決めて作られています。また、NULLを許容しないカラムがあったりします。
この関数がそのまま実行されてしまうことは避けなければなりません。
Errorを投げる
これを解決するのに、「Errorを投げる」ということをします。
今まで、0以下の数値が入力された場合にはalertを表示していました。
しかしalertは「警告」であり、「エラー」ではありません。
JavaScriptにエラーなのだということをわからせるために、Errorオブジェクトを使用します。
alertをErrorに変える
function share(inputPerson, inputSP) {
if (inputPerson <= 0 || inputSP <= 0) {
alert('正の整数値で入力してください')
}
const person = parseInt(inputPerson);
const sp = parseInt(inputSP);
if (!Number.isInteger(person) || !Number.isInteger(sp)) {
alert('入力値が不正です');
}
return inputSP / inputPerson
}
......
↓
....
function share(inputPerson, inputSP) {
if (inputPerson <= 0 || inputSP <= 0) {
throw new Error('正の整数値ではない入力です') // Errorに変更
}
const person = parseInt(inputPerson);
const sp = parseInt(inputSP);
if (!Number.isInteger(person) || !Number.isInteger(sp)) {
throw new Error('入力値が不正です'); // Errorに変更
}
return inputSP / inputPerson
}
......
このままではErrorを投げているが、受け取れていません。
Errorを受け取るtry/catch文
投げたErrorを回収するために、try/catch文を使用します。
try/catch文の流れ
下記の行をtry/catchに書きかえます。
const inputSP = prompt('スプリントのSPを入力してください', 80);
const inputPerson = prompt('スプリントのタスクを分ける人数を入力してください', 8);
const result = share(inputPerson, inputSP);
alert(`一人当たりのタスク数は${result}です`);
try {
const inputSP = prompt('スプリントのSPを入力してください', 80);
const inputPerson = prompt('スプリントのタスクを分ける人数を入力してください', 8);
const result = share(inputPerson, inputSP);
alert(`一人当たりのタスク数は${result}です`);
} catch (e) {
alert(e);
}
明示されておらずわかりづらいですが、catch (e)の部分で、Errorを受け取っています。
この(e)の中に自分が投げたErrorが入っています。
試しに0以下の数値を入力してみます。
上記のようにalert内にエラーメッセージが表示されました。
成功でも失敗でも同じ処理をしたい場合のfinary文
上記の例では、0以下の数値や文字列が入力された場合は、resultに代入されないはずです。
それを確認するためには、成功/失敗いずれかが決まった後に行う必要があります。
tryの結果がどちらであっても同じ処理をしたい、というケースでは、finary文を使用します。
finaryを加えた処理の流れ
try {
const inputSP = prompt('スプリントのSPを入力してください', 80);
const inputPerson = prompt('スプリントのタスクを分ける人数を入力してください', 8);
const result = share(inputPerson, inputSP);
alert(`一人当たりのタスク数は${result}です`);
} catch (e) {
alert(e);
} finally{
console.log(result)
}
tryが失敗していればresultは、not definedになります。
resultに代入されていないことを確認して、データが変更されていないことを確認することは重要です。
実際はここにデータベース操作が入ることが多いからです。
完成
これで例外処理が組み込まれたコードになりました。
※実際のアプリではさらに細かくエラー捕捉を行います。
function share(inputPerson, inputSP) {
const person = parseInt(inputPerson);
const sp = parseInt(inputSP);
if (!Number.isInteger(person) || !Number.isInteger(sp)) {
throw new Error('入力値が不正です');
}
if (person <= 0 || sp <= 0) {
throw new Error('正の整数値で入力してください');
}
return inputSP / inputPerson
}
try {
const inputSP = prompt('スプリントのSPを入力してください', 80);
const inputPerson = prompt('スプリントのタスクを分ける人数を入力してください', 8);
const result = share(inputPerson, inputSP);
alert(`一人当たりのタスク数は${result}です`);
} catch (e) {
alert(e);
}finally{
console.log(result)
}
実際の開発では、これに非同期通信などが絡んできて簡単にはいきませんが、例外処理をシンプルに体験してくれれば幸いです。
補足:エラーを二つに分ける
上位ではとにかく全部エラーを取得していますが、エラーには二つあります。
想定できたエラーと、予期しないエラーです。
JavaScriptにはカスタムエラーという機能があり、Errorクラスを継承することで使用できます。実際のコーディングでは二つに分けて実装することが多いです。
継承
class InputError extends Error {} // Errorクラスを継承しておく
分岐
try {
const inputSP = prompt('スプリントのSPを入力してください', 80);
const inputPerson = prompt('スプリントのタスクを分ける人数を入力してください', 8);
const result = share(inputPerson, inputSP);
alert(`一人当たりのタスク数は${result}です`);} catch (e) {
if (e instanceof InputError) {
console.error(e);
location.reload();
} else if (e instanceof Error) {
console.error(e);
alert('予期しないエラーが発生しました。終了します');
}
}
カスタムエラーの継承はその時々に最適な手法があります。都度ググることを推奨します。