Edited at

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


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);
});
});
}