【JavaScript】エラー処理の基本的なところを理解する
try/catch
やらthrow
やらthen/catch
やら、そこら辺のエラー処理のことを雰囲気で何となく使っていたけれど、レベルアップするためにしっかり調べて理解したいと思いこの記事を書きました。
長々と書いていますが、メインは「非同期処理でのエラー処理」の部分です。
また、内容的には基本的なことが多めです。
エラー処理に必要な基本的な知識
まずは、エラー処理に必要な基本的な知識をおさらい。
ここでは必要な基本的知識として、以下の 3 つを取り上げています。
- try...catch
- throw 文
- エラーオブジェクト
try...catch
▶︎ 説明
try...catch
(try 文)はtry
ブロック内で例外が発生した時に、その例外をcatch
ブロックで受け取ることがでます。( = 例外が発生しなかったらcatch
ブロックは飛ばされる。)
finally
ブロックもあり、こちらは例外が発生した・しないに関係なく最終的に必ず実行されるブロックです。
try {
// 実行される処理
} catch (error) {
// 例外が発生した場合に実行される処理
} finally {
// 必ず実行される処理
}
▶︎ 詳細
この処理はtry
ブロックと最低 1 つ以上のcatch
ブロックかfinally
ブロックが必要になります。
つまり、try 文は以下のパターンが存在するということです。
try...catch
try...finally
try...catch...finally
try
文が入れ子になった場合
■ try
文を入れ子にすることも可能です。
入れ子になった場合、入れ子のtry
文にcatch
ブロックがあればそのcatch
ブロックで例外をキャッチします。
もしも、入れ子のtry
文にcatch
ブロックがなければ外側のcatch
ブロックで例外をキャッチします。
要するに、例外が発生した最も内側のcatch
ブロックで 1 度だけキャッチされるということです。
もしも外側のcatch
ブロックにキャッチさせたい場合は、内側のcatch
ブロック内でthrow
でエラーを投げれば OK です。
(throw
部分に関しては後ほど説明)
try {
try {
throw new Error("エラー発生!");
} catch (error) { // ここで例外を受け取る
console.error('入れ子のcatchブロック:', error.message);
}
} catch (error) {
console.error("catchブロック", error);
}
// => 入れ子のcatchブロック: エラー発生!
try {
try {
throw new Error("エラー発生!");
} finally {
console.log('入れ子のfinallyブロック');
}
} catch (error) { // 入れ子try文に`catch`がないので、ここで例外を受け取る
console.error("catchブロック", error.message);
}
// => 入れ子のfinallyブロック
// => catchブロック: エラー発生!
try {
try {
throw new Error("エラー発生!");
} catch (error) {
console.error('入れ子のcatchブロック:', error.message);
throw error;
}
} catch (error) { // 入れ子try文に`catch`がないので、ここで例外を受け取る
console.error("catchブロック", error.message);
}
// => 入れ子のcatchブロック: エラー発生!
// => catchブロック: エラー発生!
参考
throw 文
次に先ほど出てきたthrow
文についての説明します。
▶︎ 説明
throw
文は例外を発生させます。これをcatch
ブロックがキャッチしてくれます。
そして、例外が発生した後続の処理は実行されません。
throw 例外の式;
▶︎ 詳細
catch
ブロックがなかった場合はクラッシュ(プログラムが終了)してしまうので、防ぎたい場合はcatch
ブロックを用意して例外をキャッチさせる必要があります。
throw
文の例外の式
部分には以下の様に、プリミティブ型やオブジェクト型(後述する Error オブジェクトなど)を入れることができます。
throw "例外発生";
throw 400;
throw new Error("例外発生!");
しかし、エラー処理を行う場合はエラーオブジェクトを生成して例外を投げることが推奨されています。
理由としては、以下の 2 つがあります。
① スタックトレース[1]が取得できる( = プリミティブ型だとエラーの発生箇所を追えない)
② エラー処理の一貫性を保つことが可能( = 共通してエラーオブジェクトなので、error.message
など同じ様に扱える)
▶︎ 参考
エラーオブジェクト
エラー処理に必要な基本的な知識の最後は、エラーオブジェクトについてです。
▶︎ 説明
エラーオブジェクトは処理が実行中にエラーが発生した時に生成されます。
try {
// funcメソッドは存在しない->エラー発生
func();
} catch (error) {
// errorにエラーオブジェクトが渡る
console.error(error);
}
ReferenceError: func is not defined
エラーオブジェクトを自作で生成して、throw
文で例外として投げることも可能です。
try {
// エラーオブジェクトを生成->`throw`で例外を投げる
throw new Error("エラーが発生!");
} catch (error) {
// `throw`で投げられたエラーオブジェクトを`catch`
console.error(error);
}
Error: エラーが発生!
▶︎ 詳細
エラーの種類
エラーにはいくつかの種類が存在します。これらは ES の仕様で定義されているエラーオブジェクトです。
一部例を挙げるけれど、詳しくはこちらを読めば OK かと。
Error
ReferenceError
SyntaxError
TypeError
など。
上で挙げた 2 つ目以降のエラーオブジェクトは、Error
オブジェクトを継承しています。
なので、存在するプロパティなど(message
プロパティなど)は一致しています。
`Error`オブジェクト ─┬─ `ReferenceError`オブジェクト
├── `SyntaxError`オブジェクト
〜〜〜〜〜〜〜〜〜〜
└── `TypeError`オブジェクト
あまり説明しすぎると記事が長くなってしまうので、分かりにくかったらこちらを読んでいただければ分かりやすいかと!
エラーオブジェクトのプロパティ
エラーオブジェクトにはいくつかのプロパティが用意されています。
代表的なものをいくつかピックアップ。
-
name
プロパティ- エラーの名称
-
Error
とかReferenceError
とかTypeError
とか
-
massage
プロパティ- エラーメッセージ
- エラーオブジェクトを生成する時の第一引数に入ってくる奴
try {
throw new Error('ここがエラーオブジェクトのmessageプロパティになる!');
} catch (error) {
console.error(error);
console.error(error.name);
console.error(error.message);
}
// error
Error: ここがエラーオブジェクトのmessageプロパティになる!
// error.name
Error
// error.message
ここがエラーオブジェクトのmessageプロパティになる!
繰り返しになりますが、エラーオブジェクトはname
プロパティやmassage
プロパティなど、共通したプロパティがあってエラー処理の一貫性を保ちやすいと言う理由が 1 つの理由としてある為、throw
で例外を投げる時はエラーオブジェクトが推奨されいるという訳です。
スタックトレースが追えない問題
こちらが参考になります。
▶︎ 参考
エラー処理
ここからは実際にエラー処理をどの様に行うかを整理していきます。
同期処理編
同期処理に関してはtry...catch
の説明と被るので飛ばしても問題ないですが、非同期処理との対比のため記述します。
例えば、string 型以外はthrow
で例外を投げる処理があるとします。
const simpleFunc = (text) => {
if (typeof text !== "string") {
throw TypeError("type is not string");
}
return "type is string";
};
simpleFunc("文字列"); // OK
simpleFunc(23); // NG(プログラムが停止する)
string 型以外はthrow
で例外を投げるのですが、このままでは受け取ってくれる場所がないのでプログラムが停止してしまします。
そこで、try...catch
を使用します。
const simpleFunc = (text) => {
if (typeof text !== "string") {
throw TypeError("type is not string");
}
return "type is string";
};
try {
simpleFunc(23);
} catch (error) {
console.error(error);
}
TypeError: type is not string
非同期処理編
ここがこの記事のメイン。(自分の中で)
非同期的な処理としてはHTTPリクエスト(axios,fetchなど)
とかイベント(clickなど)
とかタイマー(setTimeoutなど)
とかがそれに該当します。
▶︎ 同期処理と非同期処理との違い
まずは非同期処理が、同期処理とどの様に異なるかを見てみましょう。
(実際にCodeSandboxなどで試してみると分かりやすいと思います。)
try {
throw new Error('同期的なエラー');
} catch (error) {
console.error('catchブロック:', error.message);
}
console.log('この行は実行されます');
catchブロック:同期的なエラー
この行は実行されます
↑
同期処理は上から順番にtryブロックのthrow文
→catchブロックのconsole.error
→外側のconsole.log
という流れで実行されます。
try {
// `setTimeout`を使用した非同期処理
setTimeout(() => {
throw new Error('非同期的なエラー');
}, 1000);
} catch (error) {
console.error('catchブロック:', error.message);
}
console.log('この行は実行されます');
この行は実行されます
↑
setTimeout
関数のコールバック関数が 1 秒(1000 ミリ秒)に実行されて、throw
で例外を投げます。
しかし、その時には既にtry...catch
の実行が終了した後なので、例外をcatch
ブロックでキャッチできずにプログラムが終了してしまいます。
この非同期処理のエラー処理を解決するために導入された[2]のが、Promise
やasync/await
です。
▶︎ Promise によるエラー処理
上記のsetTimeout
関数の非同期処理をPromise
でエラー処理してみましょう。
実行順序は
この行は実行されます
↓
catchブロック:非同期的なエラー
の順番で、例外が処理されるのを待ってから後続処理が実行される様にしたいと思います。
const timer = () => {
return new Promise((_, reject) => {
setTimeout(() => {
reject(new Error('非同期的なエラー'))
}, 1000)
})
}
timer()
.catch((error) => console.error(error.message))
.finally(() => console.log('この行は実行されます'))
これで、1 秒後に処理が実行されて期待した結果になります。
この行は実行されます
catchブロック:非同期的なエラー
▶︎ 説明
timer
関数は、new Promise()
で Promise オブジェクトを生成して、Promise オブジェクトを返しています。
Promise オブジェクトを生成する時、第一引数にresolve
、第二引数にreject
を取ります。
Promise の状態が解決[3]されたら、チェーンメソッドのthen
やcatch
,finally
へと処理が続きます。
非同期処理が成功(resolve)したらthen
に続き、失敗(reject)したらcatch
に続きます[4]。finally
は成功しても失敗しても最終的に呼ばれます。
ですので、上記のコードは
①timer
関数を実行
②timer
関数内の Promise の状態を解決(setTimeout
関数により 1 秒に reject)
③reject されたのでcatch
の処理が実行
④ 最終的にfinally
の処理が実行
という流れになります。
▶︎ async/await によるエラー処理
async/await
は HTTP リクエストを例にしてみましょう。
今回は fetchAPI を利用して、取得したデータを表示する処理を想定します。
const fetchApi = async () => {
try {
const res = await fetch('https://sample.com/api/items');
if (!res.ok) {
throw new Error('例外が発生!');
}
return res.data;
} catch (error) {
throw error;
}
};
fetchApi()
.then(data => console.log(data))
.catch(error => console.error(error));
▶︎ 説明
まず最初に重要なことは、関数の前にasync
をつけると、その関数は必ず Promise オブジェクトを返します。
なので、同じくthen
やcatch
などのチェーンメソッドが使用可能です。
また、await
を使用すると非同期通信が完了するまで次の処理を待ちます。
処理が成功すれば後続処理は続きますが、失敗すればその場でthrow
で例外を投げてcatch
でキャッチします。
なので、try...catch
を使用してcatch
ブロックでもキャッチすることができます。
ですので、上記のコードは
①fetchApi
関数を実行
②try...catch
のtry
ブロック内を実行
③fetchAPI で HTTP リクエストを行う
リクエストが成功した場合
④ 変数res
にレスポンスデータを代入
⑤if 文を実行
⑥(成功した場合)return で Promise オブジェクトを返す
⑦fetchApi
のチェーンメソッドのthen
メソッドでconsole.log
を実行
リクエストが失敗した場合
④throw
で例外を投げる
⑤try...catch
のcatch
ブロックで例外をキャッチ
⑥throw
で外側にエラーオブジェクトを投げる
⑦fetchApi
のチェーンメソッドのcatch
メソッドで ⑥ をキャッチしてconsole.error
が実行
▶︎ [補足]fetch と axios ではエラーハンドリングが異なる
▶︎ 参考
参考
最後になりましたが、認識が間違えていたり、誤解を招く文章などがあれば教えていただけると嬉しいです。
ありがとうございました。
-
スタックトレース...プログラムの実行過程を記録した内容。どの処理をどの様な順序で実行したかを追うことができます。 ↩︎
-
ES2015 以前はエラーファーストコールバックというルールでエラー処理を行っていたみたいです。しかし、ただのルールだった為書き方が統一されていなくても問題ありませんでした。それでは問題が出てくる為、ES2015 で非同期処理を扱う為の
Promise
というビルドインオブジェクトが導入されました。 ↩︎ -
Promise オブジェクトの初期状態は
pending
です。そこからresolve
されたら成功の状態を表すFulfilled
になりthen
へ。reject
されたら失敗の状態を表すRejected
になりcatch
へ。 ↩︎ -
正確には、失敗(reject)した場合は
then
の第二引数で取得することが可能です。 ↩︎
Discussion