Expressで非同期処理をおこなうミドルウェアの動作を確認する

Expressミドルウェアの挙動、特に非同期処理をおこなうにエラーが発生した場合の挙動が気になったので試してみた。 ミドルウェアで非同期処理をするのは前処理でデータベースに登録するケース等を想定している。

挙動を確認した実装はGitHubにおいた:nodejs-module-labo/express-middleware at main · s1r-J/nodejs-module-labo

実装で気をつけるポイント

先にポイントをまとめておく。

  • next()は複数回呼び出しても問題ない
  • ミドルウェアでエラーを発生されるときはnext(err);のように呼び出す
    • 呼び出さずにthrowするだけだとExpressがハングしてタイムアウトのエラーが発生する
  • ミドルウェアで発生したエラーはエラーハンドリングでキャッチできる
  • (エラーハンドリングでキャッチした場合など)レスポンスを複数回返すとエラーになるのでres.headersSentで確認する

実装例と挙動

非同期処理が正常に実行される

ミドルウェアで非同期処理が正常に実行される場合の実装例(抜粋、全体はGitHubにある)は以下のとおり。

app.use('/no-async', (req, res, next) => {
    // 非同期的に前処理をするミドルウェア
    console.log('no-async middleware');

    setTimeout(() => {
        console.log('no-async sleep');
        next(); // ①
    }, 3000);

    next(); // ②
});

app.get('/no-async', function (req, res) { // ③
    console.log('Call no-async.');
    res.send('Express response: no-async');
    console.log('---');
});

挙動を解説する。

  1. 非同期なのでsetTimeoutのコールバック処理は飛ばされて3000ミリ秒待つことなく、②のnext()が実行される。
  2. ②のnext()によってパスの処理(③)が実行され、サーバからレスポンスが返される。
  3. 3000ミリ秒後にコンソールに「no-async sleep」と表示される。
    • ①のnext()は実行されても再度③が呼ばれることはない。

非同期処理でエラーが発生する

ミドルウェアでの非同期処理でエラーが発生する場合の実装例(抜粋、全体はGitHubにある)は以下のとおり。

app.use('/no-async-error', (req, res, next) => {
    // 非同期的に前処理をするときにエラーが発生するミドルウェア
    console.log('no-async-error middleware');

    setTimeout(() => {
        try {
            if (true) {
                throw new Error('no-async-error');
            } else {
                // エラーが発生しなかったらnextを呼ぶ
                next();
            }
        } catch (err) {
            next(err); // ④
        }
}, 3000);

    next();  // ⑤
});

app.get('/no-async-error', function (req, res) { // ⑥
    console.log('Call error.');
    res.send('Express response: no-async-error');
    console.log('---');
});

// ===エラーハンドリング===
app.use((err, req, res, next) => {  // ⑦
    console.log(`Error: ${err.name}`);

    if (res.headersSent) {  // ⑧
        // 非同期的に処理が実施されるとレスポンスが返却されている可能性がある
        console.log('response is already sent.')
        // レスポンスを複数回返すとエラーになる
        // res.status(500).send('Express response: error');  // ⑨
    } else {
        res.status(500).send('Express response: error');
    }
    console.log('---');
});

挙動を解説する。

  1. 非同期なのでsetTimeoutのコールバック処理は飛ばされて3000ミリ秒待つことなく、⑤のnext()が実行される。
  2. ⑤のnext()によってパスの処理(⑥)が実行され、サーバからレスポンスが返される。
  3. 3000ミリ秒後にsetTimeoutのコールバックでエラーが発生し、④のnext(err)が呼び出される。
  4. ④のnext(err)は⑦のエラーハンドリングでキャッチされる。
  5. ⑧ではレスポンスが既に返却されているかを確認している。
    • レスポンスを複数回返すとエラー(Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client)が発生する。(⑨のコメントアウトを戻すとエラーになる)
    • res.headersSentはレスポンスが返却済の場合にはtrue、返却していない場合にはfalseを返却するう
    • 参考:Express 4.x - API リファレンス

おわりに

非同期のときに気になる挙動については確認できた。

レスポンスを複数回返すとエラーになるので注意が必要だ。 今回エラーハンドリングにres.headersSentを使った確認を実装した。パスのほうで時間がかかる処理があるならば、レスポンスを返却済でないのかを確認する処理を入れる、またはミドルウェアのエラーを握りつぶす(ログにだけは出しておく)ような実装をおこなう必要があるだろう。