同期処理と非同期処理の間でエラーを渡す
エラー処理の引き渡しの資料はQiitaでもTipsとしていっぱい転がっている。しかし可能性として有りうる同期処理・Promise・Async/Await相互のエラー受け渡しについて言及したものは少ない。そこで、それらをできるだけ網羅してみたつもりである。
読みやすくなる前提知識は以下。
- 自前のエラーはインスタンスの生成(
new Error
)で作る - エラーはthrowで投げる
- 同期処理中のエラーはtry-catch構文で拾う
- PromiseのエラーはPromise.catchメソッドで拾う
- Async関数内のエラーはthrowで投げ、try-catch構文で拾う
この上で、コードと実行結果を交えて、Uncaughtなエラーをうまく扱っていく。
調べたきっかけ
とあるOSSを使ってみたが、非同期処理の結果が返ってこない。そのときはエラー処理が問題なのかどうかの切り分けができず、苦労することに。
イベントループ、try-catch、非同期処理、Async関数、エラーオブジェクト…
エラーをハンドルしたいだけなのに、なぜこうもMDNのドキュメントを行き来せねばならず、加えて Promise.all
を紹介されなければならないのか。
それらを深追いした末に、エラーを非同期処理中で発生させたものを同期処理中にしっかりキャッチしてもらうコードを練習するに至った。
投稿のきっかけ
私はN予備校生のsatsukizzzです。N予備校のWebプログラミング講座では、Node.jsを用いてサーバーサイドを含めJavaScriptで書くという経験をしました。その後に上記のようなことが起きました。
せっかくある程度のレベルで調べ上げたわけなので、N校受講生用の2020年アドベントカレンダーに…と思っていたのに。年末のやっつけ仕事でやろうとした私がアホで、12/25を最後にアドベントカレンダーは閉幕していました。
一方で私の友人関係では同じようにJavaScriptを使っている人たちも多いため、遅ればせながら投稿することとなりました。
環境とブラウザ
- クライアントサイドのJavaScript
- Google Chrome 86.0.4240あたり / Firefox 81.0.1あたり
Node.jsでは確認していない(next関数も重要であることは承知している)。
なお、全てのエラー表示の画像はGoogle Chrome 87.0.4280のDeveloper Toolを用いて取得している。
ケース
同期処理から同期処理へ
const tryThrownErrorInSyncToSync = () => {
try {
throw new Error('in sync');
} catch (e) {
console.error(e);
}
}
tryThrownErrorInSyncToSync();
Promiseから同期処理へ(uncaught)
const tryRejectedErrorInPromiseToSync = () => {
try {
new Promise((resolve, reject) => reject(new Error('in promise; this is an uncaught error')));
} catch (e) {
console.error(e);
}
}
tryRejectedErrorInPromiseToSync();
Async関数から同期処理へ
const rejectAsync = async (errorMessage) => {
throw new Error(errorMessage);
}; // rejectAsync関数は実はPromiseのエラーを返すなら何にでも変えられる
const throwPromise = (errorMessage) => {
return new Promise((resolve, reject) => reject(new Error(errorMessage)));
}; // つまり、代わりにこのthrowPromise関数でもよい
const tryThrownErrorInAsyncToSync = async () => {
try {
rejectAsync('uncaught async error in reject async');
} catch (e) {
console.error(e);
}
try {
await rejectAsync('caught async error in reject async');
} catch (e) {
console.error(e);
}
return;
}
tryThrownErrorInAsyncToSync();
同期処理からPromiseへ;rejectで
new Promise((resolve, reject) => {
reject(new Error('catch rejected error in sync to promise'))
})
.catch(console.error);
同期処理からPromiseへ;throwで
new Promise((resolve, reject) => {
throw new Error('catch thrown error in sync to promise; same as rejected one');
})
.catch(console.error);
PromiseからPromiseへ;コメント付加
//if you need a Promise object with arguments then make a function which returns Promise object. put that function object in Promise.then or Promise.catch directly as below
const returnRejectedPromise = (error) => {
return new Promise((resolve, reject) => {
error.message = 'rejected in promise, and ' + error.message;
reject(error);
});
}
Promise.reject(new Error('catch rejected error in promise chain'))
.catch(returnRejectedPromise) // added some information and re-rejected
.catch(console.error); // executed
new Promise((resolve, reject) => {
returnRejectedPromise(new Error('cannot catch rejected error in promise to promise; please chain those promises'));
})
.catch(console.error); // not executed
Async関数からPromiseへ;コメント付加
// using async function instead of returnRejectedPromise
const throwPromiseWithAsync = async (error) => {
error.message = 'thrown in async, and ' + error.message;
throw error;
}
Promise.reject(new Error('catch rejected error in promise chain'))
.catch(throwPromiseWithAsync) // added some information and re-rejected(thrown in async)
.catch(console.error); // executed
new Promise((resolve, reject) => {
throwPromiseWithAsync(new Error('cannot catch thrown error in async to promise; please chain those promises'));
})
.catch(console.error); // not executed
Promiseから同期処理へ;Resolveしてしまった場合
const promiseCatchResolvedErrorInPromiseToSync = () => {
new Promise((resolve, reject) => {
resolve(new Error('resolved with an error in promise; this is caught in fulfilled'));
})
.then(value => {console.log(value);}) // catches as "value" of the error
.catch(error => {console.error(error);}); // not executed
}
promiseCatchResolvedErrorInPromiseToSync();
つまり、もしエラー終了させたいならthen(value => {console.error(value);})
としてやったらいいとおもう。病気。
Promiseから同期処理へ
const promiseCatchRejectedErrorInPromiseToSync = () => {
new Promise((resolve, reject) => {
reject(new Error('rejected with an error in promise'));
})
.catch(e => console.error(e));
}
promiseCatchRejectedErrorInPromiseToSync();
Promiseから同期処理へ;エラーオブジェクトを生成しなかった場合
const promiseCatchRejectedInPromiseToSync = () => {
new Promise((resolve, reject) => {
reject('rejected without an error in promise');
})
.catch(e => console.error(e));
}
promiseCatchRejectedInPromiseToSync();
▼を押して開くと現れる、ブラウザによるスタックトレースのみとなる(エラーオブジェクトのスタックトレースがない)。
async関数からPromiseへ;returnした場合
//Promise内でAsync functionを実行しエラーを伝える
const asyncReturnError = async () => {
return new Error('returned with an error in async; this is caught in fulfilled');
};
new Promise((resolve, reject) => {
asyncReturnError()
.then(value => {resolve(value);}) //executed
.catch(error => {reject(error);});
})
.then(value => {console.log(value);}) //executed
.catch(error => {console.error(error);});
エラーオブジェクトのスタックトレースが表示されるが、エラーの色をしていない。
また、ブラウザのスタックトレースが生成されない。
async関数からPromiseへ;throwした場合
// throwの場合
const asyncThrowError = async () => {
throw new Error('throw an error in async; this is caught in rejected');
};
new Promise((resolve, reject) => {
asyncThrowError()
.then(value => {resolve(value);})
.catch(error => {reject(error);}); //executed
})
.then(value => {console.log(value);})
.catch(error => {console.error(error);}); //executed
promiseからasyncへ
//make errors in Promise and transfer them to Async function
async function testErrorsInPromiseToAsync() {
try {
await new Promise((resolve, reject) => {
reject(new Error('rejected in promise to async try block with await(but not useful because the scope of variables is try)'));
});
} catch (error) {
console.error(error);
}
new Promise((resolve, reject) => {
reject(new Error('error in rejected promise to async function without await; this is uncaught'));
})
.catch(error => {throw error;});
await new Promise((resolve, reject) => {
reject(new Error('error in rejected promise to async function with await'));
})
.catch(error => {throw error;});
}
testErrorsInPromiseToAsync()
.catch(console.error);
おまけ: setTimeout
のラップ
promiseでsetTimeoutをラップ
「setTimeoutはPromiseでラップする」とは例えばこのように。
const delayedResolve = (ms) => {
return new Promise(resolve => {setTimeout(resolve, ms)});
};
const delayedReject = (ms, error) => {
return new Promise((resolve, reject) => {setTimeout(reject(error), ms)});
};
そのうえで、
const throwAfterTimeout = async () => {
await delayedReject(1000, new Error('error in trying delayedReject'))
.catch(console.error);
}
const throwInSetTimeout = async () => {
try {
await setTimeout(() => {
throw new Error('error in trying setTimeout; this is uncaught');
}, 2000);
} catch (error) {
console.error(error);
}
}
const promiseCatchAfterTimeout = () => {
delayedReject(3000, new Error('error in Promise.catching delayedReject'))
.catch(console.error);
}
throwAfterTimeout();
throwInSetTimeout();
promiseCatchAfterTimeout();
async関数でsetTimeoutをラップ
const asyncTimeout = async (ms, errorOrValue) => {
return new Promise((resolve, reject) => {
if(errorOrValue instanceof Error) setTimeout(() => {reject(errorOrValue)}, ms);
setTimeout(() => {resolve(errorOrValue);}, ms);
});
}
asyncTimeout(4000, 'delayed resolve in async timeout')
.then(console.log) //executed
.catch(console.error);
asyncTimeout(4000, new Error('delayed error in async timeout'))
.then(console.log)
.catch(console.error); //executed
雑記
2020-12-27 公開日
この記事は自分自身へのクイックドキュメントとして存在しています。
一方で、気になるエラーや載せてほしい例があれば
- 記事へのコメント
- 私のtwitter: satsukizzzへのアクション(リプライ等) ... Qiita同様スチームミルクを落とす画像
- N予備校slackのsatsukizzzへのアクション(メンション等) ... ピーチティーの画像
をしてもらえれば追加します。そのときは、皆さんのクイックドキュメントになればなと思います。
詳しい方は用語の勘違いの指摘などしていただけると大変ありがたいです。
年末のやっつけで2か月前に試したものを一つ一つ切り出しただけであるため、後々整理すると思われますが、閲覧者の疑問点に合わせて分類するのもわかりやすいかな?と思うので、まずは公開しました。