【Express.js】非同期処理の個人的ベストプラクティス (async/await)

## Express で非同期処理
サーバサイドで Node.js を使うユースケースとしては APIサーバ (BFF、SSR 含む) が考えられるが、
主な処理 (DB との接続、http 通信など) は基本的に非同期処理になっている

非同期処理のライブラリは色々あるが現在は標準に組み込まれた Promise を使うのが主流となっており、これを使えばコールバック地獄は避けられるが、できれば async/await を使って直感的に書きたい

[JavaScriptの同期、非同期、コールバック、プロミス辺りを整理してみる](https://qiita.com/YoshikiNakamura/items/732ded26c85a7f771a27)
[async/await 入門（JavaScript）](https://qiita.com/soarflat/items/1a9613e023200bbebcb3)

しかし結局 await は async の中でしか書けないため、Express を利用する場合は実質トップレベルの `RequestHandler` 内でどう処理するかという問題が生じる

### アンチパターン: そのまま async をつける

```javascript:user.js
const express = require('express');
const router = express.Router();

router.get('/user/:id', async (req, res) => {
  const id = req.params.id;
  // reject した場合、router が非同期例外を捌けないので NG
  const user = await getUser(id);
  res.status(200).json(user);
});
```

深く考えずにこう書いてしまうと、await で reject されたときに処理できなくなってしまうので良くない

### パターン1: async 付きの即時関数で囲む

```javascript:user.js
const express = require('express');
const router = express.Router();

router.get('/users', (req, res, next) => {
  (async () => {

    const id = req.params.id;
    if (!id) {
      // throw で投げられた error も catch できる
      throw new Error('Params not found');
    }

    // 通常の reject も catch できる
    const user = await getUser(id);
    res.status(200).json(user);
  
  })().catch(next);
})

module.exports = router;

```

async 付きの即時関数で囲むことで、非同期例外を catch できるようになる
そこで `nextFunction` を渡してやれば、catch された `error` が `nextFunction` の引数に渡されて実行されるので、 Express のエラーハンドリングで処理することができるのがポイント

副次的な効果として、async 内で普通に例外を投げても catch されるので実質 try/catch の役割も果たしている
個人的にはこの効果が地味にありがたくてよく使っている

### パターン2: Wrap 関数を自前で実装する

毎回 async の即時関数を書くのは面倒なので wrap する
非同期例外も普通の例外も wrap 関数 (正確には wrap 関数の戻り値の関数) が catch して `nextFunction` に渡すようになるので、パターン1と同様にエラー処理に繋げられる

参考: [Asynchronous Error Handling in Express with Promises, Generators and ES7](https://strongloop.com/strongblog/async-error-handling-expressjs-es7-promises-generators/)

```javascript:user.js
const wrap = fn => (req, res, next) => fn(req, res, next).catch(next);

router.get('/user/:id', wrap(async (req, res) => {
  const id = req.params.id;
  if (!id) {
    // throw で投げられた error も catch できる
    throw new Error('UserID is not found');
  }

  // 通常の reject も catch できる
  const user = await getUser(id);
  res.status(200).json(user);
}));
```

ちなみに Wrap 関数自体は、async 付きの `RequestHandler` を引数として受け取って、
**非同期例外を catch して `nextFunction` に渡す処理を加えた `RequestHandler`**
を返すということをやっているだけ

わかりやすく TypeScript で書いた場合↓

```typescript:wrap.ts
import { RequestHandler, Request, Response, NextFunction } from 'express'

interface PromiseRequestHandler {
  (req: Request, res: Response, next: NextFunction): Promise<any>
}

function wrap(fn: PromiseRequestHandler): RequestHandler {
  return (req, res, next) => fn(req, res, next).catch(next)
}
```

だいぶ見通しが良くなったが、JavaScript のままだと VSCode で補完が効かなくなるでなんとかしたい

### パターン3: `Router` 側で非同期例外を処理させる

`express-promise-router` というライブラリで `Promise` を処理できる `Router` が使えるようになるので、1番自然な形で記述できる

参考: [Express with async/await](https://node-postgres.com/guides/async-express)

```javascript:user.js
const router = require('express-promise-router')();

router.get('/user/:id', async (req, res) => {
  const id = req.params.id;
  if (!id) {
    // throw で投げられた error も catch できる
    throw new Error('UserID is not found');
  }

  // 通常の reject も catch できる
  const user = await getUser(id);
  res.status(200).json(user);
});
```

ライブラリ側が型定義に対応しているので VSCode でも補完が効くようになった
-> 結局効いていなかったりするので**パターン2 or 3**を TypeScript で実装する方が単純かもしれない

```typescript:user.ts
import Router from 'express-promise-router'
const router = Router()

router.get('/user/:id', async (req, res) => {
  // 略
})
```

## 結論
**TypeScript はいいぞ**

ちなみに Express 5 では `Promise` を扱えるようになるらしいので期待
https://github.com/expressjs/express/pull/2237
最初のアンチパターンの形で書けるような仕様になってくれると嬉しい…

### 【おまけ】 callback ベースの API を `Promise` でラップする

`Promise` を返す API が用意されていなければ、自分で `return new Promise` する関数を作ってあげれば良い
Rx でいう所の `Observer.create` のようなイメージ

参考: [今更だけどPromise入門](https://qiita.com/koki_cheese/items/c559da338a3d307c9d88#new-promisecallback)

```javascript
const stringify = require('csv-stringify');

function stringifyPromise(records, options) {
  return new Promise((resolve, reject) => {
    stringify(records, options, (err, output) => {
      if (err) {
        return reject(err);
      }
      resolve(output);
    });
  });
}
```