0
1

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.

Callback 使いから Promise マスターにクラスチェンジするぞ!

Posted at

現在私は、非同期処理をするときは callback を使っています。
ですが、ネストが深くなるにつれて混乱してしまいます。( ゚Д゚)<メダパニ‼

Node.js には promise という callback 地獄を解消するためのオブジェクトが用意されています。
promise での処理を覚えれば、見やすいコーディングが可能です!

…と、言うことは理解できているのですが、何故か未だにあやふやな promise 処理。
こりゃもう、身体で覚えるしかねぇ!( ゚Д゚)
憧れの PROMISE マスターに、なりたいな。ならなくちゃ。絶対になってやるー!(*‘∀‘)

幸い(?)なことに前回作った LINEWORKS の BOT 登録プログラムが callback 地獄になっているので、こちらを promise に書き換えてみたいと思います。

(前回の記事)API を使って LINEWORKS BOT を登録する

registerBot.js

const api_id = "api id";
const consumer_key = "consumer key";
const token = "server list token";
const account_id = "ryo_saeba@hunter.city";
const domain_id = 00000000;
const callback_url = "https;//www.xyz.jp/callback";

// トーク Bot のテナント登録
const request = require('request');
const uri_text = "https://apis.worksmobile.com/" + api_id;
let options = {
    uri: uri_text + "/message/registerBot/v4",
    headers: {
        "Content-type": "application/json",
        "consumerKey": consumer_key,
        "Authorization": "Bearer " + token
    },
    json: {
        "name": "test bot",
        "photoUrl": "https://developers.worksmobile.com/favicon.png",
        "description": "defeat the promise process bot",
        "managerList": [account_id]
    }
};
request.post(options, (error, response, body) => {
    if(body.errorMessage) return;
    let botNo = body.botNo;
    // トーク Bot のドメイン登録
    options.uri = uri_text + "/message/registerBotDomain/v3"
    options.json = {
        "botNo": botNo,
        "domainId": domain_id,
        "usePublic": true,
        "usePermission": false
    }
    request.post(options, (error, response, body) => {
        if(body.errorMessage) return;
        // メッセージ受信サーバー追加
        options.uri = uri_text + "/message/setCallback/v2";
        options.json = {
            "botNo": botNo,
            "callbackUrl": callback_url,
            "callbackEventList": ["text", "sticker", "image"]
        };
        request.post(options, (error, response, body) => {
            if(body.errorMessage) return;
            // トーク Bot からメッセージ送信
            options.uri =  uri_text + "/message/sendMessage/v2",
            options.json = {
                "botNo": botNo,
                "accountId": account_id,
                "content": {"type":"text","text":"BOT 登録が完了しました(^ω^)"}
            };
            request.post(options, (error, response, body) => {
                console.log(body);
                if(body.errorMessage) return;
                console.log("success");
            });
        });
    });
});

このコードを promise 処理したコードに置き換えていくのですが、このあとしばらく私の恥ずかしい失敗談が続きますので**「お前の失敗なんざどうでもいいから成功したコードを見せてくれ!」**という一般的なご意見をお持ちの方は、是非!読み飛ばして一番最後の完成コードをご確認くださいませ。

promise に置き換えるには(失敗談)

さぁ、それではこの callback たちを promise に置き換えてみましょう!
「簡単だよ! .then でつなげばいいんだよ!」という先輩エンジニアのアドバイスの元、よく理解していないまま私が書いたコードがこちらです!

registerBot.js

// パラメータ等は前回と同じなので割愛
const request = require('request');
const uri_text = "https://apis.worksmobile.com/" + api_id;

// トーク Bot のテナント登録
new Promise((resolve,reject) => {
    request.post(options, (error, response, body) => {
        (body.errorMessage) ? reject() : resolve(body.botNo);
    })
}).then((botNo) => { // トーク Bot のドメイン登録
    options.uri = uri_text + "/message/registerBotDomain/v3"
    options.json = {
        "botNo": botNo,
        "domainId": domain_id,
        "usePublic": true,
        "usePermission": false
    }
    request.post(options, (error, response, body) => {
        (body.errorMessage) ? reject() : resolve(botNo);
    })
}).then((botNo) => { // メッセージ受信サーバー追加
    options.uri = uri_text + "/message/setCallback/v2";
    options.json = {
        "botNo": botNo,
        "callbackUrl": callback_url,
        "callbackEventList": ["text", "sticker", "image"]
    };
    request.post(options, (error, response, body) => {
        (body.errorMessage) ? reject() : resolve(botNo);
    })
}).then((botNo) => { // トーク Bot からメッセージ送信
    options.uri =  uri_text + "/message/sendMessage/v2",
    options.json = {
        "botNo": botNo,
        "accountId": account_id,
        "content": {"type":"text","text":"BOT 登録が完了しました(^ω^)"}
    };
    request.post(options, (error, response, body) => {
        if(body.errorMessage) reject();
        console.log("success");
        resolve();
    });
});

今見ると、ひっどいですねぇ~。恥さらしですな。
のちに、このコードを見た先輩が「なんでこうなるのか、本気でわけわからん」とマジ顔でおっしゃられました。
きっと、今この記事を読んでいる皆様も同じ顔をしているのでしょう。
恥ずかしさのあまり、つい山籠もりをしてしたくなりますが、後回しにして先に進みます。

上記コードですが、もちろんエラーになりました!(/・ω・)/

ReferenceError: reject is not defined

ようは「なんで何度も reject してんの?」ってことです。
コンピュータさん、頭の悪い子でごめんなさい。

promise にとって、reject は一回こっきり。
最初に new するときだけなのです。

元のコードでやっていたエラー処理を return から単に reject に置き換えたのがいけない。
理解が浅いからこういうことになるのです。
まったく恥ずかしいやつですね!(/ω\)

.catch でエラー処理をする

ってなわけで、たくさんの reject をひとつにすべく .catch でエラー処理をしていきます!
promise オブジェクトでは .catch でエラー処理いっぺんに指定できてしまうんです!
なんて素晴らしい仕様!
処理が複雑になり、.then の数が増えていっても、エラー処理は1つだけ。
わかりやすいし、見やすいですね~。
ってなわけで .catch を追加してエラー処理を1つにまとめたものがコチラ。

registerBot.js

// パラメータ等は前回と同じなので割愛
const request = require('request');
const uri_text = "https://apis.worksmobile.com/" + api_id;

// トーク Bot のテナント登録
new Promise((resolve,reject) => {
    request.post(options, (error, response, body) => {
        (body.errorMessage) ? reject() : resolve(body.botNo);
    })
}).then((botNo) => { // トーク Bot のドメイン登録
    options.uri = uri_text + "/message/registerBotDomain/v3"
    options.json = {
        "botNo": botNo,
        "domainId": domain_id,
        "usePublic": true,
        "usePermission": false
    }
    request.post(options, () => {});
}).then((botNo) => { // メッセージ受信サーバー追加
    options.uri = uri_text + "/message/setCallback/v2";
    options.json = {
        "botNo": botNo,
        "callbackUrl": callback_url,
        "callbackEventList": ["text", "sticker", "image"]
    };
    request.post(options, () => {});
}).then((botNo) => { // トーク Bot からメッセージ送信
    options.uri =  uri_text + "/message/sendMessage/v2",
    options.json = {
        "botNo": botNo,
        "accountId": account_id,
        "content": {"type":"text","text":"BOT 登録が完了しました(^ω^)"}
    };
    request.post(options, () => {});
}).catch((err) => { // エラー処理
    console.log(err);
});

おー!見やすくなったぞー(*´▽`)
などと最初は喜んだおバカな私。もちろんエラーになりましたとさ。ぎゃふん。

TypeError: Cannot read property 'botNo' of undefined

まぁ、ちゃんとエラーが catch できていたので良しとしましょう。

return で次の .then に値を渡す

さて、肝心のエラー内容ですが「botNo が空っぽだよ」と言われています。
そう、return していないので、次の .then に何にも渡していないんですよね!
超絶おバカですね。

return botNo;

return 文をそれぞれの .then に追加して、これで大丈夫だろう!と実行した私。
エラーメッセージ出なかったぞ!やったぁ!成功だ!

と、喜んだのも束の間。待てど暮らせど登録完了メッセージが送信されてこない。
「トーク Bot からメッセージ送信 API」の実行ログを見てみると、以下のエラーが。

{ errorMessage: 'Service fail, HTTP/1.1 400 Bad Request, {"code":400,"message":"Bad Request Parameters: Cannot access bot"}', errorCode: '090', code: 'SERVICE_UNAVAILABLE' }

ふむ。Bot にアクセスできないとな?・・・なんで?
これ、2つ目の「トーク Bot のドメイン登録」をしていないメッセージ送信すると起きる現象なんですよね。
試しにもう一度トーク Bot からメッセージ送信 API を叩いてみる。…成功。

つまり、だ。今はメッセージ送信ができて、登録時には、できない。
.then で非同期処理をしているはずなのに、ドメイン登録時の request の結果を待たずに次の .then に進んでいって、ドメイン登録完了前にメッセージを送信している?
つまーり!非同期処理になってないっ!!どうして!?

.then 内の request は結局 promise 化していない

どうしても何も、私がよく理解していないまま使ったのが悪いのです。反省。orz
色々と調べてみたところ、.then の中の request.post() は promise 化されていない模様。
promise 化されていないってことは非同期処理されていないってことで、もちろん順番はめちゃめちゃに。

よし、トーク Bot のテナント登録の際に promise 化した request を使おう!
と、そこでふと思った。いちいち request を promise 化するの、面倒くさくね?
面倒くさいということは!誰かが便利なものを作っているはず!

そうして私は request-promise と出会いました。先輩の皆様、ありがとうございます。
では、さっそく npm install request-promise してコードを書き替えていきたいと思います。

registerBot.js

// パラメータ等は前回と同じなので割愛
const request = require('request-promise');

let botNo;
// トーク Bot のテナント登録
request(options).then((body) => {
    botNo = body.botNo;
    // トーク Bot のドメイン登録
    options.uri = uri_text + "/message/registerBotDomain/v3"
    options.json = {
        "botNo": botNo,
        "domainId": domain_id,
        "usePublic": true,
        "usePermission": false
    }
    return request(options);
}).then((body) => {
    // メッセージ受信サーバー追加
    options.uri = uri_text + "/message/setCallback/v2";
    options.json = {
        "botNo": botNo,
        "callbackUrl": callback_url,
        "callbackEventList": ["text", "sticker", "image"]
    };
    return request(options);
}).then((body) => {
    // トーク Bot からメッセージ送信
    options.uri =  uri_text + "/message/sendMessage/v2",
    options.json = {
        "botNo": botNo,
        "accountId": account_id,
        "content": {"type":"text","text":"BOT 登録が完了しました(^ω^)"}
    };
    return request(options);
}).catch((err) => {
    console.log(err);
});

return request(options); に書き換えているので、botNo が次の .then に渡っていかないことに注意。
一番最初の bodybotNo を変数に入れておきましょう!

さぁ、実行してみましょう!ドン!

1558664439.png

だーいせーこーう♪ (*‘∀‘)<promise bot ゲットだぜ!

思った通りの動作をしてくれたので、良かった良かったホッと一息。
これで終わった!と、思いきや…。
実はある落とし穴があったことに、このときの私は気づいていなかったのだ。。。

エラー処理ができていない!?

さっき、.catch でエラー処理の設定をしました。
API ID や TOKEN などのパラメータの入力が間違っていた場合には、.catch に飛んでいってエラーを表示してくれるはず。
しかし、.catch には飛ばず、次の .then に進んでいってしまい、エラーを連発しているのです!

register-tenant
{ errorMessage: 'API ID not exists',
  errorCode: '052',
  code: 'NOT_FOUND' }
register-domain
{ errorMessage: 'API ID not exists',
  errorCode: '052',
  code: 'NOT_FOUND' }
register-callback
{ errorMessage: 'API ID not exists',
  errorCode: '052',
  code: 'NOT_FOUND' }
send-message
{ errorMessage: 'API ID not exists',
  errorCode: '052',
  code: 'NOT_FOUND' }

ナントイウコトデショウ

LINEWORKS API のエラーは .catch しない

これ、今回大変勉強になったところです。
例えばなんですが、request する際に uriNULL であるとか、パラメータの型指定が間違っているとかだと、ちゃんと .catch してくれるのですよ。
でも、LINEWORKS API からの返答がエラーだった場合には .catch してくれないのです。

request 自体は成功しているから、API がエラーだって言おうが request は成功してるもん!
ってことなんですね。

API に届いた時点で、成功。
その先でエラーだった場合には返ってきた値を見て .catch に投げる必要があるんですね。
try ~ catch における throw をする必要があるんですね。

throw しないで、reject しよう

JavaScript Promise の本に書かれていますが、promise オブジェクトを使うときは Promise.reject を使います。
body の中を見てエラーだったら Promise.reject(body); とすれば .catch に飛んでエラー表示してくれます。

LINEWORKS API からの Response がエラーかどうかを判別するには、errorCode または errorMessage が含まれているかを判定すれば良いでしょう。
なので、下記一文を諸所に追加します。

if(body.errorMessage) return Promise.reject(body);

これで、ついに完成となりました!

完成したコードです!

registerBot.js
const api_id = "api id";
const consumer_key = "consumer key";
const token = "server list token";
const account_id = "ryo_saeba@hunter.city";
const domain_id = 00000000;
const callback_url = "https;//www.xyz.jp/callback";

const request = require('request-promise');
const uri_text = "https://apis.worksmobile.com/" + api_id;

// トーク Bot のテナント登録
let options = {
    method: "POST",
    uri: uri_text + "/message/registerBot/v4",
    headers: {
        "Content-type": "application/json",
        "consumerKey": consumer_key,
        "Authorization": "Bearer " + token
    },
    json: {
        "name": "test bot",
        "photoUrl": "https://developers.worksmobile.com/favicon.png",
        "description": "defeat the promise process bot",
        "managerList": [account_id]
    }
};
let botNo;
console.log("register-tenant");
request(options).then((body) => {
    if(body.errorMessage) return Promise.reject(body);
    console.log(body);
    botNo = body.botNo;
    // トーク Bot のドメイン登録
    options.uri = uri_text + "/message/registerBotDomain/v3"
    options.json = {
        "botNo": botNo,
        "domainId": domain_id,
        "usePublic": true,
        "usePermission": false
    }
    console.log("register-domain");
    return request(options);
}).then((body) => {
    if(body.errorMessage) return Promise.reject(body);
    console.log(body);
    // メッセージ受信サーバー追加
    options.uri = uri_text + "/message/setCallback/v2";
    options.json = {
        "botNo": botNo,
        "callbackUrl": callback_url,
        "callbackEventList": ["text", "sticker", "image"]
    };
    console.log("register-callback");
    return request(options);
}).then((body) => {
    if(body.errorMessage) return Promise.reject(body);
    console.log(body);
    // トーク Bot からメッセージ送信
    options.uri =  uri_text + "/message/sendMessage/v2",
    options.json = {
        "botNo": botNo,
        "accountId": account_id,
        "content": {"type":"text","text":"BOT 登録が完了しました(^ω^)"}
    };
    console.log("send-message");
    return request(options);
}).then((body) => {
    if(body.errorMessage) return Promise.reject(body);
    console.log(body);
}).catch((err) => {
    console.log("error:")
    console.log(err);
});

うん、長い!( ゚Д゚)

おわりに

ここまでお付き合いいただきありがとうございました。いや、本当に。

ここまでの道のりも長かったし、比例して記事も長くなってしまいました。
結局、コードの長さ的には Callback と変わらなかったなぁ。

でも!ネストしていないから見やすいし、promise オブジェクトの構造がわかってると、こっちの方が拡張や修正はしやすいかも!

promise.finally とか Promise.all とか、まだ仲良くなれていない子たちがいるから、憧れの PROMISE マスターへの道のりはまだまだ遠いけど、諦めずに頑張りたいと思います!(^ω^)

ではまた!(^^)/

参考にさせていただきましたm(_ _)m

JavaScript Promiseの本
LINEWORKS Developers

JavaScriptの同期、非同期、コールバック、プロミス辺りを整理してみる
今更だけどPromise入門
CoffeeScriptでPromiseを使ったときにハマった
request-promiseを使ったHTTPクライアントを作る

0
1
0

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
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?