最初に
この記事では、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 のエラーを収集する