解説記事は色々流れているけど、実際に触ってみないと覚えないので。
歴史の整理
async / await は Promiseと関連している。まず、時系列を整理。
- 昔の Node では Promise が独自実装されており。2010年頃 deprecate された、という話がある。
- Node.js v0.11.13 より、ES2015/ES6 Promise が利用可能に
- Node.js v7 より ES2017/ES7 async/await が利用可能に。
Promise
async / await を理解するにあたってはまずPromiseから。
例えば次のようなHTTPリクエストの非同期処理を考える。
1 2 3 4 5 6 7 8 9 10 11 12 |
request = (options) => { const req = https.request(options, (res) => { res.on('data', (body) => { return; // ★2 }); }); req.on('error', (e) => { throw e; }); req.end(); // ★1 } |
この例では、★1まで処理が進行して関数を抜けてしまってから、★2が実行される。
node.js の イベントループモデルはそもそもがそういうものなのだけど、
やっぱり同期処理的に書いたり処理した方がいいよね、ということで可能にしたのがPromiseで、
次のように既存処理を括って書ける。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
request = (options) => { return new Promise((resolve, reject) => { const req = https.request(options, (res) => { res.on('data', (body) => { resolve(body); // ★2 }); }); req.on('error', (e) => { reject(e); // ★2 }); req.end(); // ★1 }); // ★3 } |
この場合、★1まで処理が進行した後、★2で応答を返すまで Promise が処理を返さない。
★2が実行されると、★3以降の処理が実行される、というもの。
ちなみに、上記の例で、呼び出し側は次のようになる。
1 2 3 4 5 6 7 |
request(options) .then((body) => { console.log(body); }) .catch((e) => { console.log(e); }); |
async/await
async/awaitは、上記 Promise の呼び出しを簡略化して、
かつ、処理の流れ自体を同期化する仕組み。
async
async の簡単な説明、
- async をつけた関数は、Promiseを返す
- async function が return した場合、Promise は戻り値を resolve する。
- async function が 例外等を throw した場合、Promise は、それを reject する。
要は、既存の関数をそのまま、同期処理に変えることができる。
しかし、ポイントがあって、あくまで「その関数」内の return / throw に作用するので、上記に示したような、ライブラリのコールバックには対応できない。(と思う)
なので、async で Promise 実装を簡潔化しよう、と思っても、Promise は残らざるを得ない。(はず)
※ async function って、「非同期で処理される関数です」じゃなくて、「非同期処理を含む関数(だけどPromiseで同期的に処理できる)です」ってことですね。ちょっと混乱した。
await
対して、await。
- async function 内でしか使えない。
- await を指定した関数の Promise が返却されるまで処理を待機する。
なので、await で同期的に待つ、という事をしたい場合は、その関数自身が async function である必要があり、
つまりは、Promise を返すことになり、つまりは、同期的に処理されうる関数になる。
なので、await で待つことを決めた時点で、その関数は同期的な関数になり、その関数が async function になることで、その上位の関数にも、同期関数になるかどうかの選択を与える、という連鎖が続く。
これが最上位まで続くと、そのコールスタック全てが同期的に処理されることが、言語的に保証される。
というものなんだな、という理解。
Promise を待つ場合、従来の then, catch が使えるので、await を使う必要は必ずしも無いが、処理を簡潔にさせる等の理由で使うという判断ができる。
上記の呼び出し例は、await を使うと、次のように書ける。
1 |
await const body = request(options); |
エラー処理のために、try – catch も、Promise 同様に then, catch を使うこともできるが、
catch を使った方が処理が簡潔に書ける。
1 2 3 4 |
await const body = request(options) .catch((e) => { console.log(e); }); |
async/await の伝搬
async function にすると、Promise が返されるので、呼び出し元では、await 等の Promise 的な処理を行う必要がある。なので、非同期処理が無いにも関わらず、async 化しておくのは無駄のように見える。
async/await モデルにおいては、async function にした時点で、呼び出し元には await を使うことになり、その関数も必然的に async function になるので、という連鎖が続くので、全てを async function にする必要が出てくる。
そうさせないためには、あえて、async function だけど同期的に処理されていることが保証されていて呼び出し元に伝搬させる必要がないから、await は用いず、Promise の処理を行って、async をつけない、ということを行う必要がある。
ということが正解なのかがよくわからない。
例
async/await を使った場合の、伝搬例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
// async を使った方が簡潔だけど、ライブラリ等の都合で async にできない場合は Promise実装 function funcA() { return new Promise((resolve, reject) => { const req = https.request(options, (res) => { res.on('data', (body) => { resolve(body); }); }); }); } // funcA を呼び出す場合、await が使える。その場合、async を使う必要がある。 async function funcB() { const body = await funcA(); return true; } // funcB が同期的に処理されることがわかっている場合、async しないために、await しなくてもよい。 function funcC() { funcB(); } // でも非同期処理が有りうるなら、await を使うことになるだろう async function funcD() { await funcB(); } // 最上位からコールスタック全体を同期的に処理したいなら、async を伝搬して使うことになるだろう async function main() { await funcD() } |
Lambda の Node.js
Node.js 8.10 ランタイムから、イベントハンドラ自体を async で記述できるようになっている。
1 2 3 4 5 |
exports.myHandler = async function(event, context) { return "some success message”; // or // throw new Error(“some error type”); } |
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/nodejs-prog-model-handler.html
なので、中の処理を同期的に書いてもいいよ、ということになっている。
async function では、await が使えるので、中で async function を定義して、コールしていくという作りにできる。