最初に
この記事では、JavaScriptのエラーに関する知識やベストプラクティスについて記述しています。
文字列を直接throwしない
throwステートメントを使用してエラーを発生させる際、次のように文字列を直接throwするべきではありません。
throw 'やばいエラー💩';
これは、上記のように文字列をthrowした場合、スタックトレースが取得できないためです。これでは、問題の発生箇所を特定することができません。
エラーをthrowする場合は、以下のようにErrorオブジェクトを使用するのが正解です。
throw new Error('やばいエラー💩');
なお、文字列のthrowはESLintのルール(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()を実行しますが、以下の図で示すエラーのように、それらの関数呼び出しがスタックに現れません。
そこで、コードを以下のようにします。
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に生成されます。
この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 のエラーを収集する

