Express 4 はミドルウェア内で async が書けるが、ラッパー関数はあった方が良い

Express 4 系だと、次のように async が書けるようだ。

// async を使用
app.use('/', async (req, res) => {
  // await を使用
  const result = await something( req.body.id );
  res.send(result);
});

ただし上のコードだと、await 部分でエラーが発生した時に next() が実行されないので、エラーハンドリングミドルウェアに処理が移らない。

キャッチされないエラーが発生するといつまでもレスポンスされず、おかしな動きになる。

次のように書けば、エラー時にエラーハンドリングミドルウェアに処理を流せる。

app.use('/', async (req, res) => {
  try {
    const result = await something( req.body.id );
    res.send(result);
  }
  catch(error) {
    next(error);
  }
});

// エラーハンドリングミドルウェア
app.use((error, req, res, next) => {
  console.error('Error Handling Middleware');
  res.status(500).send(error);
});

しかし、catch 句の変数 errorundefinednull などの空値だとnext() と同義になり、コレだとエラーハンドリングミドルウェアに処理が移動しない。

例えば以下のようなコードの、 部分が実行されてしまった場合だ。

app.use('/', async (req, res) => {
  try {
    const result = await something( req.body.id );
    
    if(result.type === 'failed') {
      throw new Error();  // ★
    }
    else if(result.type === 'rejected') {
      await Promise.reject();  // ★
    }
    
    res.send(result);
  }
  catch(error) {
    next(error);  // error が undefined な値だとエラーハンドリングミドルウェアが呼ばれない
  }
});

// エラーハンドリングミドルウェア
app.use((error, req, res, next) => {
  console.error('Error Handling Middleware');
  res.status(500).send(error);
});

この場合も、いつまでもレスポンスされない動きになる。

catch(error) {
  next(error || 'ERROR IS NULL');
}

こんな感じで、error が空だった時は何らかの値を渡してやれば、とりあえずはなんとかなる。


ところで、ルーティング定義ごとに try / catch を忘れずに書き、next(error || 'ERROR') といった処理を書くのは面倒臭い。

そこで、次のようなラッパー関数を作っておくと良いだろう。

function asyncWrap(fn) {
  return (req, res, next) => {
    return fn(req, res, next)
      .catch((error) => {
        next(error || 'ERROR IS NULL');
      });
  };
}

使う時はこんな感じ。

// asyncWrap() 内に async function を書く
app.use('/', asyncWrap(async (req, res) => {
  const result = await something( req.body.id );
  res.send(result);
}));

// エラーハンドリングミドルウェア
app.use((error, req, res, next) => {
  console.error('Error Handling Middleware');
  res.status(500).send(error);
});

TypeScript 化しておくとこんな感じのラッパー関数になるだろう。

import * as express from 'express';

/** Promise を返すインターフェース */
interface PromiseRequestHandler {
  (req: express.Request, res: express.Response, next: express.NextFunction): Promise<unknown>;
}

/**
 * 内部で async を使用できるラッパー関数・例外発生時も次のミドルウェアで処理できるよう next() を呼ぶ
 * 
 * Express 4 からは直接 async ミドルウェアを書けるが、例外を catch し next(error) を明示的に呼ぶ必要があるため、ラッパー関数を用意した
 */
export default function asyncWrap(fn: PromiseRequestHandler): express.RequestHandler {
  return (req: express.Request, res: express.Response, next: express.NextFunction): Promise<unknown> => fn(req, res, next)
    .catch((error: unknown) => {
      next(error || 'ERROR IS NULL');  // 変数 error が null や undefined だとエラーミドルウェアに移動しないので適当な値を入れておく
    });
}

以上。