【JavaScript】エラー処理の基本的なところを理解する

11 min read読了の目安(約10000字

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文にcatchブロックがあるパターン
try {
  try {
    throw new Error("エラー発生!");
  } catch (error) { // ここで例外を受け取る
    console.error('入れ子のcatchブロック:', error.message);
  }
} catch (error) {
  console.error("catchブロック", error);
}

// => 入れ子のcatchブロック: エラー発生!
入れ子try文にcatchブロックがないパターン
try {
  try {
    throw new Error("エラー発生!");
  } finally {
    console.log('入れ子のfinallyブロック');
  }
} catch (error) {  // 入れ子try文に`catch`がないので、ここで例外を受け取る
  console.error("catchブロック", error.message);
}

// => 入れ子のfinallyブロック
// => catchブロック: エラー発生!
入れ子try文のcatchブロックでthrowして、外側の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ブロック: エラー発生!

参考

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/try...catch

throw文

次に先ほど出てきたthrow文についての説明します。

▶︎ 説明

throw文は例外を発生させます。これをcatchブロックがキャッチしてくれます。
そして、例外が発生した後続の処理は実行されません。

throw 例外の式

▶︎ 詳細

catchブロックがなかった場合はクラッシュ(プログラムが終了)してしまうので、防ぎたい場合はcatchブロックを用意して例外をキャッチさせる必要があります。

throw文の例外の式部分には以下の様に、プリミティブ型やオブジェクト型(後述するErrorオブジェクトなど)を入れることができます。

throw '例外発生';
throw 400;
throw new Error('例外発生!');

しかし、エラー処理を行う場合はエラーオブジェクトを生成して例外を投げることが推奨されています。
理由としては、以下の2つがあります。
①スタックトレース[1]が取得できる( = プリミティブ型だとエラーの発生箇所を追えない)
②エラー処理の一貫性を保つことが可能( = 共通してエラーオブジェクトなので、error.messageなど同じ様に扱える)

▶︎ 参考

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/throw

エラーオブジェクト

エラー処理に必要な基本的な知識の最後は、エラーオブジェクトについてです。

ここではエラーオブジェクトとErrorオブジェクトは別の意味として使い分けをする。(ややこしいと思ったらごめんなさい...)
エラーオブジェクト => ReferenceErrorオブジェクトなどエラーの種類全般を指す
Errorオブジェクト => そのままErrorオブジェクトを指す

▶︎ 説明

エラーオブジェクトは処理が実行中にエラーが発生した時に生成されます。

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プロパティ
    • エラーメッセージ
    • エラーオブジェクトを生成する時の第一引数に入ってくる奴
Errorオブジェクトのプロパティをみる
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で例外を投げる時はエラーオブジェクトが推奨されいるという訳です。

スタックトレースが追えない問題

こちらが参考になります。

https://qiita.com/Tsuyoshi84/items/c50fbbf30a2af387efdf#thenの外側でerrorオブジェクトを生成する

▶︎ 参考

https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Control_flow_and_error_handling
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Error
https://jsprimer.net/basic/error-try-catch/

エラー処理

ここからは実際にエラー処理をどの様に行うかを整理していきます。

同期処理編

同期処理に関しては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]のが、Promiseasync/awaitです。

▶︎ Promiseによるエラー処理

上記のsetTimeout関数の非同期処理をPromiseでエラー処理してみましょう。
実行順序は

この行は実行されます
↓
catchブロック:非同期的なエラー

の順番で、例外が処理されるのを待ってから後続処理が実行される様にしたいと思います。

Promise
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]されたら、チェーンメソッドのthencatch,finallyへと処理が続きます。
非同期処理が成功(resolve)したらthenに続き、失敗(reject)したらcatchに続きます[4]finallyは成功しても失敗しても最終的に呼ばれます。

ですので、上記のコードは
timer関数を実行
timer関数内のPromiseの状態を解決(setTimeout関数により1秒にreject)
③rejectされたのでcatchの処理が実行
④最終的にfinallyの処理が実行
という流れになります。

▶︎ async/awaitによるエラー処理

async/awaitはHTTPリクエストを例にしてみましょう。
今回はfetchAPIを利用して、取得したデータを表示する処理を想定します。

async
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オブジェクトを返します
なので、同じくthencatchなどのチェーンメソッドが使用可能です。

また、awaitを使用すると非同期通信が完了するまで次の処理を待ちます。
処理が成功すれば後続処理は続きますが、失敗すればその場でthrowで例外を投げてcatchでキャッチします。
なので、try...catchを使用してcatchブロックでもキャッチすることができます。

ですので、上記のコードは
fetchApi関数を実行
try...catchtryブロック内を実行
③fetchAPIでHTTPリクエストを行う
リクエストが成功した場合
④変数resにレスポンスデータを代入
⑤if文を実行
⑥(成功した場合)returnでPromiseオブジェクトを返す
fetchApiのチェーンメソッドのthenメソッドでconsole.logを実行
リクエストが失敗した場合
throwで例外を投げる
try...catchcatchブロックで例外をキャッチ
throwで外側にエラーオブジェクトを投げる
fetchApiのチェーンメソッドのcatchメソッドで⑥をキャッチしてconsole.errorが実行

awaitはトップレベルのコードでは動作できないので注意が必要です。
asyncの関数でラップする必要があります。

▶︎ [補足]fetchとaxiosではエラーハンドリングが異なる

https://zenn.dev/syu/articles/9840082d1a6633

▶︎ 参考

https://jsprimer.net/basic/async/
https://ja.javascript.info/async-await
https://zenn.dev/yukiota/articles/cb53ea21d7cf3994861a

参考

https://mya-ake.com/slides/nuxt-axios-error-handling#0
https://qiita.com/kiyodori/items/da434d169755cbb20447
https://teratail.com/questions/147133
https://qiita.com/legokichi/items/b14bf7dbb0cf041955d6

最後になりましたが、認識が間違えていたり、誤解を招く文章などがあれば教えていただけると嬉しいです。
ありがとうございました。

脚注
  1. スタックトレース...プログラムの実行過程を記録した内容。どの処理をどの様な順序で実行したかを追うことができます。 ↩︎

  2. ES2015以前はエラーファーストコールバックというルールでエラー処理を行っていたみたいです。しかし、ただのルールだった為書き方が統一されていなくても問題ありませんでした。それでは問題が出てくる為、ES2015で非同期処理を扱う為のPromiseというビルドインオブジェクトが導入されました。 ↩︎

  3. Promiseオブジェクトの初期状態はpendingです。そこからresolveされたら成功の状態を表すFulfilledになりthenへ。rejectされたら失敗の状態を表すRejectedになりcatchへ。 ↩︎

  4. 正確には、失敗(reject)した場合はthenの第二引数で取得することが可能です。 ↩︎