161
117

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-10-11

Express で非同期処理

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

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

JavaScriptの同期、非同期、コールバック、プロミス辺りを整理してみる
async/await 入門(JavaScript)

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

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

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 付きの即時関数で囲む

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 された errornextFunction の引数に渡されて実行されるので、 Express のエラーハンドリングで処理することができるのがポイント

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

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

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

参考: Asynchronous Error Handling in Express with Promises, Generators and ES7

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 で書いた場合↓

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

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 で実装する方が単純かもしれない

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入門

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);
    });
  });
}
161
117
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
161
117

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?