L is Bエンジニアブログ

ビジネス用メッセンジャーdirectのエンジニアによるブログ

LisBエンジニアブログ

ビジネスチャットdirectのエンジニアブログ

チャットボットでAsync/Awaitを使ってみました

こんにちは。相変わらずチャットボットのコードばかり書いている鍋山です。
チャットボットはNode.jsで書いていますが、その他には最近はGolangを少し書いたりしていて、同僚から型のある言語はいいだろうと言われました。
ちょっとだけいいなと思いました。

はじめに

さて、Node.js v7の途中から追加されたAsync/Awaitを使ってHubot(daab)を書いてみました。
(Async/AwaitはJavaScriptの標準仕様のECMAScript2017(ES2017)で追加された新仕様です)
Promise登場前のコールバックが連続するようなコードから逃れ、Promise登場によってチェインでつなぐというコードが書けるようになりました。
それでもまだスッキリとしたコードを目指すために、Async/Awaitを使います。

この記事で検証したプラットフォームはdirectです。

direct4b.com

使用したNode.jsのバージョンはv8.1.2です。
(2017.6現在は、v8.x.xの本番利用は推奨されていません)

https://nodejs.org/en/

ボットのコード

Async/Awaitを使わずに書いたボットのコード

この記事でのサンプルコードでは、主にscripts/app.jsを修正します。

おなじみのボットのディレクトリ構造

├── bin
│   ├── hubot
│   └── hubot.cmd
├── external-scripts.json
├── package-lock.json
├── package.json
└── scripts
    └── app.js // <- このファイルを修正(元々のファイルは削除)

最初のコード状態。
ペアトークにて「ping」というメッセージ送信にのみ反応するコードです。

scripts/app.js

WEBブラウザでdirect上でのやり取りを確認します。

f:id:nabeyama:20170620192738p:plain

Async/Awaitを使って書いたボットのコード

次に、asyncを書き加えたコードです。

asyncを/PING$/iの後ろに付け加えただけですが、これでも動作します。

ですが、これでは何も動きは変わらないので、awaitを使って、2秒ほどres.send('PONG')を遅延させてみます。

awaitを書き加えたコード

setTimeoutで2秒待たせた後にres.send('PONG')が動いてくれるはずです。

Async/Awaitの利点

Async/Awaitを使うと、Promiseのpendingの状態を待ち、fulfilledかrejectedとなるまで次の処理に移りません。Promiseをチェインでつなぐコードと比較してみます。

Promiseをチェインでつないだコード

Async/Awaitを使ったコード

インデントが揃い、どのような処理を呼び出しているかも比較しやすくなっていることがわかります。 また、返り値がある場合に、その取得や利用を容易にすることができることもAsync/Awaitの利点です。この利点についてもPromiseをチェインでつないだコードと比較してみます。

Promiseをチェインでつないだ返り値のあるコード

Async/Awaitを使った返り値のあるコード

同期的な関数を呼び出し返り値を取得しているかのようにコードを書くことができました。arg1は、Promiseのコードではスコープが変わる為、res.sendにそのまま渡すことができませんが、Async/Awaitを使ったコードでは、そのまま渡すことができます。このような引数の使い勝手も向上しています。

例外処理

次に、async内で例外が起きた場合にどうなるか見てみます。

async内で例外が起こるコード

実行(PING送信)すると、コンソールにWARNINGが出力されました。

(node:24808) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): TypeError: Assignment to constant variable.
(node:24808) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

UnhandledPromiseRejectionWarningと例外をキャッチできていないことを警告されています。

async内のawaitでrejectされるコード

こちらも実行(PING送信)すると、同様のWARNINGが出力されました。

エラーハンドリングの為の関数を用意します。
(プロダクションで使うには、もう少し適切な処理が必要です)

エラーハンドリングの関数を利用して、robot#Eventの処理を書き直します。

async内で例外が起こるコード(+エラーハンドリング)

実行(PING送信)すると、先ほどのWARNING(UnhandledPromiseRejectionWarning)は出力されませんでした。

[catchAsync]
エラー: TypeError: Assignment to constant variable.
.........

ボットが「Error.」と返してくれます。

f:id:nabeyama:20170620192838p:plain

async内のawaitでrejectされるコード(+エラーハンドリング)

実行(PING送信)すると、同じくWARNING(UnhandledPromiseRejectionWarning)は出力されませんでした。

[catchAsync]
エラー: This is reject!

これでunhandledRejectionのWARNINGは出なくなりました。

基本的には、非同期処理はawaitして例外をキャッチできるようにします。
awaitさせずに非同期処理で例外が出た場合には、unhandledRejectionが出力されますので、好ましい状態ではありません。

ここまでのコードにあった、catchAsyncのコードを外部ファイルに切り出して置いておき、読み込んで使用するようにコードを整理しておきます。

コード切り出し後のディレクトリです。

├── bin
│   ├── hubot
│   └── hubot.cmd
├── external-scripts.json
├── package-lock.json
├── package.json
└── scripts
    ├── app.js
    └── models
        └── catch-async.js // <- 新しく設置

models/catch-async.js

app.js

近年では、半年おきにメジャーバージョンが上がるという、開発のスピードが速いNode.jsですが、その度にパフォーマンスが向上したり、便利な機能が追加されていったりと、開発者や利用者の為になることがとても多いです。

まだまだチャットボットの開発需要は続く中、人手の足りない歯がゆさがあります。
弊社でチャットボットの開発をしたい方は、こちらからご応募待っています!

https://l-is-b.com/ja/recruit/322/l-is-b.com