🔭

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

2021/04/01に公開

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


エラーオブジェクト

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

▶︎ 説明

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

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が実行

▶︎ [補足]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の第二引数で取得することが可能です。 ↩︎

GitHubで編集を提案

Discussion