はじめに
Expressを学び始めて、エラーハンドラの扱い方にいくつかのパターンがあるため、実装方法を整理。
本記事では、第3弾として非同期関数における、エラーハンドラの実装方法をまとめる。
- Expressのエラー処理まとめ 【第1弾】Expressのエラーハンミドラの定義
- Expressのエラー処理まとめ 【第2弾】バリデーションのミドルウェア
- Expressのエラー処理まとめ 【第3弾】非同期関数のエラーハンドラ(本記事)
1.非同期処理のエラーハンドラの実装方法
非同期関数内のエラーについてはExpressの標準のエラーでは取得することができない。非同期関数内のエラーをハンドリングするにはtry-catch構文で括り、ハンドリングする必要がある。
// 非同期処理
const delayedError = (delay) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
const num = Math.floor(Math.random() * 10 + 1);
return resolve(num);
}, delay);
});
};
// 非同期関数のエラーハンドリング
app.get('/number1', async (req, res, next) => {
try {
const num = await delayedError(1000);
if (num > 5) { //6以上だったら正常
res.send(`非同期処理成功:${num}`);
} else { //5以下だったらエラー
throw new AppError(`非同期処理失敗:${num}`, 401);
}
} catch (e) {
next(e);
}
});
処理の結果、非同期処理の関数内でエラーが発生した場合にエラーのハンドリングができていることが分かる。
ちなみに、try-cachを外して実行してみると
app.get('/number1', async (req, res, next) => {
const num = await delayedError(1000);
if (num > 5) {
res.send(`非同期処理成功:${num}`);
} else {
throw new AppError(`非同期処理失敗:${num}`, 401);
}
});
ということで、やはり非同期関数内のエラーハンドリングはtry-catch構文で拾ってあげる必要があることが分かった。
2.非同期関数内のエラーを取得するwrap関数を用意する
非同期関数をエラーハンドリングするために、try-catch構文で括る必要があることが分かったが、非同期関数は頻繁に記載するため、毎回try-catch構文で括るのは結構手間。そこで推奨されている方法が、非同期関数のエラーをハンドリングできる関数を1つ用意し(wrap関数という)、非同期関数を引数として渡してあげることで、その非同期関数のエラーをハンドリングする方法だ。まずは結論のコードから書いてみる。
// wrap関数
function wrapAsync(fn) {
return function (req, res, next) {
fn(req, res, next).catch(e => next(e));
}
}
// 非同期処理
app.get('/number1', wrapAsync(async (req, res) => {
const num = await delayedError(1000);
if (num > 5) {
res.send(`非同期処理成功:${num}`);
} else {
throw new AppError(`非同期処理失敗:${num}`, 401);
}
}));
処理結果も確認すると、確かにエラーのハンドリングができていることがわかる。
だけど、このwrap関数、コードの量が少ない割に、これがなぜ非同期関数をtry-catch構文で括った関数と同じになるのか理解することが難解。わかりそうで分かない...。そのため、次項から少しずつコードを変形しながら考えてみる。
3.非同期処理wrap関数の変形
STEP1
初期状態(非同期関数がtry-catch構文で括られている状態。)
app.get('/number1', async (req, res, next) => {
try {
const num = await delayedError(1000);
if (num > 5) {
res.send(`非同期処理成功:${num}`);
} else {
throw new AppError(`非同期処理失敗:${num}`, 401);
}
} catch (e) {
next(e);
}
});
STEP2
非同期関数をごっそり引き抜き、ルートハンドラのコールバック関数を返す関数を定義する。
function wrapAsync2() {
return async (req, res, next) => {
try {
const num = await delayedError(1000);
if (num > 5) {
res.send(`非同期処理成功:${num}`);
} else {
throw new AppError(`非同期処理失敗:${num}`, 401);
}
} catch (e) {
next(e);
}
}
}
app.get('/number2', wrapAsync2());
STEP3
try-catch構文内で実行したい非同期関数をfnで定義して抜き出し、wrap関数の引数に渡す。ルートハンドラで得た(req,res,next)をfnの引数に渡すために、return functoion(req,res,net){}の関数内でfnを実行する。またfnは非同期処理でPromiseを返す関数のため、エラー発生時は「.catch」としてエラーをハンドリングすることができる。
const fn = async function (req, res, next) {
const num = await delayedError(1000);
if (num > 5) {
res.send(`非同期処理成功:${num}`);
} else {
throw new AppError(`非同期処理失敗:${num}`, 401);
}
}
function wrapAsync3(fn) {
return function (req, res, next) {
fn(req, res, next).catch((e) => { //非同期処理の例外はPromiseの戻り値Rejetとしてchatcheへ
next(e)
})
}
}
app.get('/number3', wrapAsync3(fn));
STEP4
実行したい非同期関数fnをルートハンドラに入れる。
function wrapAsync4(fn) {
return function (req, res, next) {
fn(req, res, next).catch((e) => {
next(e)
})
}
}
app.get('/number4', wrapAsync4(async (req, res, next) => {
const num = await delayedError(1000);
if (num > 5) {
res.send(`非同期処理成功:${num}`);
} else {
throw new AppError(`非同期処理失敗:${num}`, 401);
}
}));
STEP5
成形する。
Promiseの構文を簡略化し、ルートハンドラの関数ではnextを利用していないため削除。nextは暗黙的に保持しており、使用がなければ削除できる。
function wrapAsync4(fn) {
return function (req, res, next) {
fn(req, res, next).catch(e => next(e));
}
}
app.get('/number5', wrapAsync4(async (req, res) => {
const num = await delayedError(1000);
if (num > 5) {
res.send(`非同期処理成功:${num}`);
} else {
throw new AppError(`非同期処理失敗:${num}`, 401);
}
}));
まとめ
非同期関数のエラーハンドラーはtry-catch構文を利用するすことで、実装できる。非同期関数を受取り、try-catch構文を1つ作ることで、他の非同期関数のエラーハンドリングに活用できる。DBを伴う処理などは非同期処理となるため、このようなエラーハンドリグを実装する必要があることが分かった。
エラー処理のまとめ全3弾終わり。