tohokuaikiのチラシの裏

技術的ネタとか。

async修飾子とawait演算子の使い方

async修飾子とawait演算子の使い方

たぶん、Qiitaなんかでは2500くらい記事がありそうなこのネタ。よくわかってなかったので自分用にまとめておく。

async修飾子とawait演算子

この2つ、よくasync/awaitみたいに紹介されてるけど並列で並べるようなものじゃないから。修飾子と演算子の違いがあります。 ここがわかってなくて、えー、

await function foo(){

みたいにして「なんでSyntaxエラーなんじゃー」とかやってた。

asyncは修飾子なので関数宣言の前に使います!

あたりまえだけど、

async function foo(){
}

bar = {
  baz: async function(){
  }
}

はいけるけど、

await function foo(){
}

bar = {
  baz: await function(){
  }
}

はSyntaxErrorくらいます。

awaitは演算子なので、式の中で使います!

加算代入演算子のように

num2 = ++ num1;
bar  = await foo();

って感じです。*1

async修飾子のお約束

async宣言された関数にはお約束があります。

<async お約束1>Promiseオブジェクトが返り値になってしまう

自動的に「なってしまう」なのです。

async function sample() {
    const result = 3;
    return result + 5;
}
RESULT = sample();
console.log(RESULT);

このRESULTは「8」ではなく、Promiseオブジェクトです。

これをasync修飾子なしで書くとこんな感じ。

function sample() {
    return new Promise(function(resolve, reject){
                           const result = 3;
                           resolve(result + 5);
                       });
}

もちろんresolve, rejectのスペルを使わなくてこれでも構いません。

function sample() {
    return new Promise(function(a, b){
                           const result = 3;
                           a(result + 5);
                       });
}

<async お約束2>async宣言された関数の返り値はthen()で受け取る

Promiseオブジェクトが返り値なら、さっきの一見返り値に見えた「8」はどうやって受け取るのかというと、thenを使います。

async function sample() {
    const result = 3;
    return result + 5;
}
promise = sample();
promise.then(function(result){
           console.log(result); 
      });

なんて面倒な…しかし、こうやるのです。

<async お約束3>async宣言された関数内で発生したエラーはcatch()で受け取る

こんな感じで

try {
   promise().then();
} catch (e){
   
}

try~catchブロックではないです。

こんな感じです

promise().then().catch(function(error){})

動くコードで書くと

async function sample2(){
    throw new Error('Error in async');
}
sample2().then().catch(function(e){ console.log(e.message); });

await演算子のお約束(というか性質)

awaitにもお約束があります。修飾子ではない演算子なので性質っていう方がいいかもしれません。

<性質1> 右オペランドがPromiseオブジェクトの場合に「待つ」

オペランドとかカッコいいこと言ってますが、ようはawaitの次に来る関数です。(正確には関数の戻り値)

これがPromiseオブジェクトでないならawaitはあってもなくても同じです。

サンプルコードを載せる前に、2つ目の性質「await演算子はasync修飾子の中でないと置けない」のでそれを踏まえたサンプルコードを書きます。

function sample(value) {
    return new Promise(function(res, rej){
                       res(value + 3);
                       });
}

async function sample2(){
    let result = await sample(4);
    console.log(result);
}
sample2(); // 7

sample関数をawaitするわけですが、その終了は何かというとsample関数が返すPromiseオブジェクト内でres()されることです。 (正確にはPromiseオブジェクトのConstruct時の第一引数の関数の第一引数の関数…ややこしい)

これだと何のうまみもないのですが、もしsample関数が時間のかかる処理だった場合、2秒くらいかかる処理だけど確実に待ちたいというこんなのだったら…

いったんPromiseオブジェクトを返しておいて、あとで処理します。

function sample(value) {
    return new Promise(function(resolve, reject){
                           setTimeout(function(){
                               resolve(value + 3);
                           }, 2000)
                       });
}

async function sample2(){
    let result = await sample(4);
    console.log(result);
}
sample2();

これをawaitを使わないでPromiseだけで書くとこんな感じです。

function sample(value) {
    return new Promise(function(resolve, reject){
                           setTimeout(function(){
                               resolve(value + 3);
                           }, 2000)
                       });
}

let result = sample(4);
result.then(function(result){ console.log(result) });

resultの取り方がちょっと面倒になりました。

await/Promiseってどこがおいしいの?

さらにawaitもPromiseも使わずに書くとこんな感じです。

let value = 4;
setTimeout(function(){
    console.log(value + 3);
}, 2000);

もちろんこれだけなら簡単なのですが、ここに潜む以下のロジック

  • 与えられた値に + 3 を足して返す
  • 与える値は4

を分離しようとすると「ほぼ無理じゃん」ってなります。

<性質2> awaitをやめさせるのは「ダメだった」場合でもよい

PromiseオブジェクトのConstruct時の第一引数の関数の第二引数の関数を実行させます。

reso()ではなくrej()をPromise内で実行して、catch()する。(try-cacheブロックではない)

function sample(value) {
    return new Promise(function(reso, rej){
                           setTimeout(function(){
                               rej("promise errror");
                           }, 2000)
                       });
}

async function sample2(){
    let result = await sample(4);
}
sample2().catch(function(e){ console.log(e); });

Errorオブジェクトの発生が使いたければこんな感じですが

function sample(value) {
    return new Promise(function(reso, rej){
                           setTimeout(function(){
                               rej(new Error("promise errror"));
                           }, 2000)
                       });
}

async function sample2(){
    let result = await sample(4);
}
sample2().catch(function(e){ console.log(e.message); });

このコードにあまり意味があるとも思えないのですが、実はこれはsample関数をasyncを使った時のコードなのです。

このコードをasyncを使うとこんな感じになります。asyncの関数で間違いが起こったらerrorを投げたいですよね。

async function sample(value) {
    throw new Error("promise errror");
}

async function sample2(){
    let result = await sample(4);
}
sample2().catch(function(e){ console.log(e.message); });

というように、catch()にはerrorが投げられてきます。

<性質3> awaitがreso()もrej()もしなかった場合

けしからんのですが、致し方ないです。

ついうっかりとreso()もrej()もしないでPromise内でreturnしてしまったとしましょう。

function sample(value) {
    return new Promise(function(reso, rej){
                           return value + 3;
                       });
}

async function sample2(){
    let result = await sample(4);
    console.log(result);  // ここにすら来ない!!
}
sample2().then(function(r){console.log(r); }).catch(function(e){ console.log(e.message); });

何も出力されません。

なぜawaitはasyncの中でなければならないか?

reso()もrej()もしない不埒な(約束破りな)awaitでも、asyncの中でなら強制終了させても実行時にはthen(), catch()が行われないだけで全体に影響は及ぼさない用にかな?と思いました。

問題

これをPromiseを使わないでasyncを使って書き直すには?

function sample(value) {
    return new Promise(function(res, rej){
                       res(value + 3);
                       });
}

async function sample2(){
    let result = await sample(4);
    console.log(result);
}
sample2(); // 7

*1:awaitはasync宣言された関数の中でないとSyntaxError起こすという条件はあるけどね。