Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
94
Help us understand the problem. What is going on with this article?
@yukin01

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

More than 1 year has passed since last update.

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);
    });
  });
}
94
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
yukin01
iOS → Firebase → インフラ/SRE 見習い
globis
グロービスは 1992 年の創業以来、社会人を対象とした MBA、人材育成の領域で Ed-Tech サービスを提供し、現在は日本 No.1 の実績があります。これらの資産と、さらに IT や AI を活用することで、アジア No.1 を目指しています。

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
94
Help us understand the problem. What is going on with this article?