What is async await?

2022/11/15に公開されました。
2022/11/15に更新されました。

JavaScriptの背景をなぞって非同期処理async await使いになろう


author: ashino

はじめに

こんにちは、Applicationチームのashinoです。

業務でReactを触ることになっていざAPIリクエストを実装してみると、非同期の文脈で登場するasync awaitの扱いに戸惑いました。

今回はJavaScriptやTypeScriptのコードを書いている時に頻出するasync awaitについて得た学びを共有していきます。

なお今回の記事は、私が得た学びを私なりの言葉やイメージで再構築してる部分もあるので、内容的におかしいところがあったら、このブログを参考・反面教師・たたき台・人のふり見て我がふり直せ的なニュアンスで捉えてご自身で学習・再定義してください。


async awaitとは

そもそも、aysnc awaitって?

非同期関数は async キーワードで宣言され、その中で await キーワードを使うことができます。 async および await キーワードを使用することで、プロミスベースの非同期の動作を、プロミスチェーンを明示的に構成する必要なく、よりすっきりとした方法で書くことができます。

引用 : https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/async_function

とりあえず引用しましたが、なるほどさっぱりわかりません!

非同期はどういうもので、JavaScriptの文脈でどう使われるのか、どうして使われるのか、についてひとつひとつ順を追って読み解いていきます。

とりあえず概要だけ知りたいって方用

  1. JSはシングルスレッド言語で快適な操作性のために様々な処理を非同期で実装する必要があった。
  2. 非同期処理はコールバック関数を使った冗長な書き方をしてたがpromiseでスッキリ書けるようになった。
  3. promiseを使ってより書きやすくなったasync await構文ができてハッピーになった。

JavaScriptはシングルスレッド言語

JavaScriptはシングルスレッド言語です。ブラウザでアプリケーション用途として使われることが多く、処理や画面の更新も行うメインスレッドをブロッキング(計算リソースを得られずハングアップになった状態)させないため、様々な関数が非同期処理になるよう実装されています。

もう一度確認しますが、JavaScriptはシングルスレッドで実行される言語です。

JavaScript はプロトタイプベースで、シングルスレッドで、動的型付けを持ち、そしてオブジェクト指向、命令型、宣言型 (関数プログラミングなど) といったスタイルをサポートするマルチパラダイムのスクリプト言語です。

引用 : https://developer.mozilla.org/ja/docs/Web/JavaScript

非同期、同期、並行処理、並列処理の違い

前述したようにJSはシングルスレッドで動作する言語であり、非同期的な実装が必須でした。

非同期処理は名前から考えるとメインスレッド以外で実行されるようにみえますが、基本的には非同期処理も同期処理と同じようにメインスレッドで実行されます。

ここで非同期ってそもそもどういう意味合いなのか。

同期処理・非同期処理・並行処理、並列処理の違いを軽く説明していきます。


同期処理

コードを順番に処理していき、1つの処理が終わるまで次の処理は行わない

なのでブロッキングされた場合に次の処理に行けなくて困ります。

ブラウザで使用されているJavaScriptでブロッキングが起きてしまうと、表示の更新やスクロールができなくなるといった問題があります。

JavaScriptでは快適な操作性を確保するために後述する非同期処理が用いられています。


非同期処理

ある処理を開始してもその処理の完了を待たずに、次の処理を実行・継続すること。

同期処理は順番に処理されますが、非同期処理は順番が来たら、サッと次の処理に譲る。そんなイメージです。

メインスレッドを止めずに別の処理を行うための手法として使われています。


並行処理

処理の主体は常に1つで、複数の処理を切り替えながら実行する。同時に実行されているかのように見える。


並列処理

複数の処理主体が、同時に複数の処理を行う。

非同期処理 in JavaScript

JavaScriptで非同期が用いられる背景について軽く説明したところで、実際に使われている代表的な非同期処理の実装例を紹介していきます。

  • callback関数
  • Promise
  • async await(本題)

callback関数

JavaScriotでの非同期処理に関して歴史的背景を調べていくと、callback関数(ある関数の引数として渡される関数のこと。ある関数の中で呼ばれて処理を完了させる。)を使った並行的な非同期処理実装があります。

setTimeout関数は非同期的な処理を実装できます。 第一引数にcallback関数、第二引数に何ms秒遅延させるかの値を渡します。

setTimeout(callback関数, delay);

まずはsetTimeout関数を使わない同期的な処理をみていきます。

console.log("処理1");
console.log("処理2");
console.log("処理3");

実行結果

処理1
処理2
処理3

これは当たり前の流れですね。

書いたプログラムが一行ずつ実行され、各行が前の行に書かれた処理の完了を待って実行されています。

次にsetTimeout関数とcallback関数を使った非同期的な処理をみていきます。

console.log("処理1");
setTimeout(() => {
  console.log("処理2");
}, 1000);
console.log("処理3");

実行結果

処理1
処理3
処理2

この実行結果から見てわかるように、setTimeout関数自体の実行は即座に完了して、 callback関数(処理2)の実行の完了は待っていません。 次の処理3が直後に実行されています。

処理3が処理された後に、遅延してきたcallback関数(処理2)が実行されているのがわかります。

同期的な処理の流れであれば、実行結果は処理1→処理2→処理3のはずですが、非同期処理を行うことで 処理1→処理3→処理2となることが確認できました。

そんなcallback関数を用いる問題としてあげられる理由として可読性が悪くなることがあります。

先ほどの例を参考に、処理1の後に処理2、処理2が実行されたら処理3という流れを非同期処理を使って書きます。

console.log("処理1");
setTimeout(() => {
  console.log("処理2");
  setTimeout(() => {
    console.log("処理3");
  }, 2000);
}, 1000);

実行結果

処理1
処理2 //1秒後に表示
処理3 //2秒後に表示

callback関数を入れ子にすることで表現できました。これでも良いのではないでしょうか?

ですが想定する処理が増え、その流れをcallback関数で表現していくと、とんでもない入れ子構造になってしまい処理の見通しが悪くなります。

console.log("処理1");
setTimeout(() => {
  console.log("処理2");
  setTimeout(() => {
    console.log("処理3");
    setTimeout(() => {
      console.log("処理4");
      setTimeout(() => {
        console.log("処理5");
      }, 4000);
    }, 3000);
  }, 2000);
}, 1000);

これがよく耳にするcallback地獄です。

さらにエラー処理をつけた場合、どれくらい醜くなってしまうかは書かなくてもわかります。

また、非同期的なエラーをキャッチできない等の問題もあります。

Promise

上記のcallback関数だと色々と大変だったので、ES2015からPromiseという非同期処理を見通しよく書くことのできる関数が追加されました。

Promiseでは非同期処理の流れを平易に書くことができます。 具体的には例外的なエラー処理や複数の非同期処理を意図した通りの順番で簡単に実現できます。また、成功時(resolve)、失敗時(reject)の処理を明示的に書くことが出来ます。

Promiseオブジェクトはnew Promise()によってインスタンス化され、いずれかの状態を持ちます。

待機 (pending): 初期状態。成功も失敗もしていません。
履行 (fulfilled): 処理が成功して完了したことを意味します。
拒否 (rejected): 処理が失敗したことを意味します。

引用:https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise

Promiseの書き方

new Promise((resolve, reject) => {
  // 同期or非同期処理
  // 処理に成功した場合、resolveを呼び、fulfilledな状態のPromiseオブジェクトを返す。
  // 処理に失敗した場合、rejectを呼び、rejectedな状態のPromiseオブジェクトを返す。
})
  .then(() => {
    // resolveの実行を待って非同期処理
  })
  .catch(() => {
    // rejectの実行を待って非同期処理
  })
  .finally(() => {
    // resolveかrejectの実行を待って非同期処理
  });

.then()や.catch()、.finally()は新たに生成されたPromiseオブジェクトを返します。

そのためメソッドチェーンを使って非同期処理を複数繋げて書くことが可能です。

console.log("処理1");
function promiseFunc() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("処理2");
      resolve(1);
    }, 1000);
  });
}
promiseFunc()
  .then(() => console.log("処理3"))
  .catch(() => console.log("Error"))
  .finally();

実行結果

処理1
処理2
処理3

非同期処理を意図した通りの順番で処理させることができました。また非同期処理の外側で例外エラーをキャッチできます。

コードの流れを見ても上から下へと順序よく流れているように見えます。

async await

Promiseが非同期処理の標準として実装されると、新たにES2017でasync awaitという構文が登場しました。

これはPromiseを使った糖衣構文(syntax sugar)です。この構文を利用することで非同期処理を同期処理と同じような書き方で書くことができるようになりました。素晴らしいですね。

asyncは、下記のように関数内で戻り値をPromiseとして返していなくても、戻り値の型をPromiseオブジェクトとして返します。暗黙的にってやつですね。

async function hoge() {
  return 1;
}

await式は、後ろに書かれた、Promiseを返却する関数の非同期処理が終わるのを待ちます。

function asyncShowMessage(message: string) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(message);
      resolve(1);
    }, 1000);
  });
}

async function asyncAwaitFunc() {
  await asyncShowMessage("処理1");
  await asyncShowMessage("処理2");
  await asyncShowMessage("処理3");
}

asyncAwaitFunc();

実行結果

処理1
処理2
処理3

このように非同期処理にawait式を使うことで、連続的な非同期処理をPromiseのメソッドチェーンを明示的に構成して記述する必要がなくなります。そのためコードがよりスッキリして可読性も上がります。

まとめ

もう一度最初の引用文を見てみましょう。今ならなんとなく理解できるのではないでしょうか。

非同期関数は async キーワードで宣言され、その中で await キーワードを使うことができます。 async および await キーワードを使用することで、プロミスベースの非同期の動作を、プロミスチェーンを明示的に構成する必要なく、よりすっきりとした方法で書くことができます。

引用 : https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/async_function

async awaitは簡単に非同期処理を書くことができて素晴らしいです。

This is async await!!




GI Cloudは事業の拡大に向けて一緒に夢を追う仲間を募集しています

当社は「クラウドで日本のIT業界を変革し、世の中をもっとハッピーに」をミッションに掲げ、Google Cloudに特化した技術者集団として、お客様にコンサルティングからシステム開発、運用・保守まで一気通貫でサービスを提供しています。

まだ小規模な事業体ですが、スタートアップならではの活気と成長性に加えて、大手総合商社である伊藤忠グループの一員としてやりがいのある案件にもどんどんチャレンジできる環境が整っています。成長意欲の高い仲間と共にスキルを磨きながら、クラウドの力で世の中をもっとハッピーにしたい。そんな我々の想いに共感できる方のエントリーをお待ちしています。

採用ページ

※本記事は、ジーアイクラウド株式会社の見解を述べたものであり、必要な調査・検討は行っているものの必ずしもその正確性や真実性を保証するものではありません。

※リンクを利用する際には、必ず出典がGIC dryaki-blogであることを明記してください。
リンクの利用によりトラブルが発生した場合、リンクを設置した方ご自身の責任で対応してください。
ジーアイクラウド株式会社はユーザーによるリンクの利用につき、如何なる責任を負うものではありません。