LoginSignup
214
168

More than 3 years have passed since last update.

JavaScriptのエラーついて知っておくべきこと

Last updated at Posted at 2018-04-17

最初に

この記事では、JavaScriptのエラーに関する知識やベストプラクティスについて記述しています。

文字列を直接throwしない

throwステートメントを使用してエラーを発生させる際、次のように文字列を直接throwするべきではありません。

throw 'やばいエラー💩';

これは、上記のように文字列をthrowした場合、スタックトレースが取得できないためです。これでは、問題の発生箇所を特定することができません。

エラーをthrowする場合は、以下のようにErrorオブジェクトを使用するのが正解です。

throw new Error('やばいエラー💩');

なお、文字列のthrowESLintのルール(no-throw-literal)で禁止させることができるので、ESLintを使用することでこのようなミスを防ぐことが可能です。

非同期のErrorはcatchできない

setTimeoutのように非同期で発生したErrorは、次のコードで示すようにtry-catchステートメントでcatchすることができません。

function throwError() {
  throw new Error('やばいエラー💩');
}

function run() {
  try {
    // 通常通りthrowErrorを実行
    throwError();

  } catch (error) {
    // throwErrorのErrorをcatchできる
    console.error(error);
  }
}

function runAsync() {
  try {
    // setTimeoutで1秒後にthrowErrorを実行
    setTimeout(throwError(), 1000);

  } catch (error) {
    // 非同期に実行すると、throwErrorのErrorをcatchできない
    console.error(error);
  }
}

ちなみに、setTimeout()を使用する場合、Promiseを使用したラッパーを用意することで、Errorをキャッチすることができます。

// setTimeoutのラッパー
function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

function runAsync() {
  delay(1000)
    .then(throwError)
    .catch(error => {
      // Errorをcatchできる
      console.error(error);
    });
}

thenの外側でErrorオブジェクトを生成する

まずは、以下のコードを見てみましょう。

function fetchUser() {
  fetch('https://something.com/user')
    .then(response => {
      if (!response.ok) {
        throw new Error('エラーだよ');
      }
    })
}

このコードでは、fetchUser()の中でfetch()を使用してユーザデータを取得しています。また、fetch().then()の中では、レスポンスがOKではなかった場合にErrorをthrowしています。この処理自体は決して間違いではありませんが、問題はここで作られたErrorオブジェクトにfetchUser()を呼び出すまでのスタックが生成されないという点にあります。つまり、どのような順序を辿って最終的に関数が呼び出されたのか、判断が難しくなってしまうのです。fetchUser()を呼び出す次の処理を見て見ましょう。

function funcA() {
  fetchUser();
}

function funcB() {
  funcA();
}

funcB();

上記のコードでは、funcB() -> funcA() -> fetchUser()という順序で最終的にfetch()を実行しますが、以下の図で示すエラーのように、それらの関数呼び出しがスタックに現れません。

image.png
※ thenの中でErrorを生成した場合のスタック

そこで、コードを以下のようにします。

function fetchUser() {
  // fetch()をコールする前にErrorオブジェクトを用意する
  // これによって、fetchUser()を呼び出すまでのstackが生成される
  const err = new Error();

  fetch('https://something.com/user')
    .then(response => {
      if (!response.ok) {
        err.message = 'エラーだよ';
        throw err;
      }
    })
}

function funcA() {
  fetchUser();
}

function funcB() {
  funcA();
}

funcB();

上記のようにfetch()を呼び出す前にErrorオブジェクトを生成しておくことで、fetchUser()を呼び出すまでのスタックがErrorに生成されます。

image.png
※ thenの外側でErrorを生成した場合のスタック

このfetchのように、非同期処理を伴う場合はErrorオブジェクトを事前に生成しておくことで、エラー発生までの経緯を明確にすることができます。

awaitにはtry-catchを使用する

awaitを使用して非同期処理を実行した場合、呼び出した処理がrejectされるとそれ以降の処理は実行されません。

function throwErrorFromPromise() {
  return new Promise(resolve => {
    throw new Error('やばいエラー💩');
  });
}

async function run() {
  await throwErrorFromPromise();

  // 次のコードは実行されない
  console.log('😟');
}

run();

このようなことが起こる理由は、awaitによって呼び出された処理がrejectされた場合、rejectされた値をthrowしてそれ以降の処理を実行しないためです。そのため、awaitで呼び出した処理がrejectされても処理を継続させる場合は、try-catchでエラーハンドリング処理を記述しておく必要があります。

async function run() {
  try {
    await throwErrorFromPromise();
  } catch(error) {
    // エラーハンドリング処理
  }

  // 次のコードは実行される
  console.log('😀');
}

その他

以下の記事では、捕捉されなかったJavaScriptエラーのデータ収集方法について述べています。こちらも一読することをお勧めします。
ユーザのブラウザで起きた JavaScript のエラーを収集する

参考

214
168
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
214
168