🦔

async / awaitについて、再確認(超初心者向け)

2024/12/24に公開

はじめに

カレーを作るとき、ご飯を炊く作業と野菜を切る作業は同時に進められますが、ご飯をお皿によそうのは、ご飯が炊き上がるまで待たなければできません。このような状況をコンピューターの世界で考えると、ご飯を炊きながら野菜を切るのは「非同期処理」、ご飯が炊き上がるまで待つのは「同期処理」と呼ばれます。そして、この同期・非同期の動作をプログラム上で指示するために使われるのが、async / await という構文です。

この記事では、この async / await をわかりやすく解説します。

async / await は、プログラムの中で「一度にすべて処理せず、完了するのを待ちながら進める」仕組みを簡単に書けるようにするための方法です。たとえば、インターネットから情報を取得する場合、データが返ってくるまで時間がかかることがあります。その間、プログラムを停止させるのではなく、他の作業を並行して進めることができるのです。

ここでは、JavaScript での async / await の使い方や注意するポイントを説明します。


基本的な使い方

  1. async とは?
    async を関数の前に付けることで、その関数は非同期であることを示します。

関数(プログラムのまとまり)の前に async を付けると、「この関数の中には、終わるのを待たなければいけない作業があるかも」とプログラムに教えることができます。

async の例
async function getMessage() {
  return "こんにちは!";
}

相手に挨拶をして、返事を画面に表示させるアプリを作ります。この関数は「こんにちは!」と返すのですが、返事がすぐに返ってこない場合を想定して(すぐに挨拶してくれない人もいるので)非同期として扱っています。

  1. await とは?
    await を使うと、「この作業が終わるのを待つよ」という指示が出せます。await は必ず async 関数の中で使用する必要があります。
await の例
async function showMessage() {
  const message = await getMessage(); // getMessageが終わるのを待つ
  console.log(message); // 結果を表示する
}

showMessage();
// 出力: こんにちは!

このプログラムでは、まず getMessage() が終了するのを待ち、その結果を message に代入します。その後、その結果を画面に表示します。
これは文字を返すだけの簡単な例ですが、たとえば、メッセージを取って来るためにネットワークに問い合わせを行う場合、返事が届くまでに数秒かかることがあります。await を付けると、返事が届くのを待ってから値を受け取ることができますが、await を付けない場合は、メッセージを受け取る前にテキストではない値が画面に出力されてしまいます。


エラーが起きたらどうする?

プログラムを実行するとにエラーが起きることもあります。たとえば、インターネットが繋がらないときです。その場合は try(やってみる)と catch(失敗したらこうする)を使います。

  • fetch() 関数について

fetch は、例えばインターネットを経由して相手からのメッセージを取得するときに使います。
jsonplaceholder.typicode.com はメッセージの代わりにレスポンスを返すテストサイトです。

エラーを拾い上げる例
async function fetchData() {
  try {
    const response = await fetch("https://jsonplaceholder.typicode.com/posts/1");
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error("返事を受け取れませんでした:", error);
  }
}

fetchData();

このように書くと、エラーでアプリが止まることを防ぐことができます。


async / await を使うときの注意点

JavaScript で非同期処理を使うとき、async は非常に便利です。でも、正しく使わないと、予想外の動きをしてしまうことがあります。
await を使うと順番に動いてくれます。ただ、何にでも await を使えば良いわけではなく、使い方は以下のように考えましょう。

方法 よい わるい
awaitを使う 順番をきっちり守って動く 一個ずつ動くから遅く感じることがある
awaitを使わない いろんなことを一気にできる 順番がバラバラになる

await を使うと RPGのようなゲームでキャラクターが順番に行動するみたいなイメージです。
一方 awaitを使わないのは、フォートナイトのようにみんなが同時に動いてワイワイしている感じです。でも、順番を決められないから困ることもありますよね。

await をつけないとどうなる?

await をつけないで async 関数を呼び出すと、関数の中の処理が「バックグラウンドで実行」されます。そのため、プログラムの処理順序が変わることがあります。

例えば、次のコードを見てみましょう。

  1. メイン関数( main() )が非同期でサブ関数( fetchData() )を実行します。
// 非同期で、web読込を行う関数(読み込み、JSON化する)
const fetchData = async (id) => {
  console.log("データを取得中..."); // タイトルを表示
  const response = await fetch("https://jsonplaceholder.typicode.com/posts/" + id);
  const data = await response.json();
  console.log(data);
  console.log("データを取得完了");
  return;
};

// 非同期で実行するメイン関数
const main = async () => {
  console.log("[main 開始]"); // メイン関数の開始表示

  // データを取得
  fetchData(1);

  // データを取得
  fetchData(2);

  console.log("[main 終了]"); // メイン関数の終了表示
};

// mainを実行。呼び先で非同期にサブ関数を呼ぶ。
main();

このコードの実行結果はたまたま次のようになりました。id が 2 -> 1 の順になっています。これは、fetchData が非同期に実行され、相手のサーバがレスポンスを返す順に処理が終わるので、順序が保証されないためです。

% node sample_load_async.js
[main 開始]
データを取得中...
データを取得中...
[main 終了]
{
  userId: 1,
  id: 2,
  title: 'qui est esse',
  body: 'est rerum tempore vitae\n' +
    'sequi sint nihil reprehenderit dolor beatae ea dolores neque\n' +
    'fugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\n' +
    'qui aperiam non debitis possimus qui neque nisi nulla'
}
データを取得完了
{
  userId: 1,
  id: 1,
  title: 'sunt aut facere repellat provident occaecati excepturi optio reprehenderit',
  body: 'quia et suscipit\n' +
    'suscipit recusandae consequuntur expedita et cum\n' +
    'reprehenderit molestiae ut ut quas totam\n' +
    'nostrum rerum est autem sunt rem eveniet architecto'
}
データを取得完了

fetchData() が始まるとすぐに "[main 終了]" が表示されます。これは、await をつけていないため、fetchData の実行を待たずに次の行に進んでしまうからです。
更に、id が 2 -> 1 の順になっています。id 順に表示させたい場合、この順番では困りますよね。このように、await を忘れると処理の順序が予想外になり、問題を引き起こすことがあります。

どうすればいい?

await をつける。
次のコードを見てみましょう。先ほどと違うのは fetchData() を呼ぶときに await をつけているだけです。

// 非同期で、web読込を行う関数(読み込み、JSON化する)
const fetchData = async (id) => {
  console.log("データを取得中..."); // タイトルを表示
  const response = await fetch("https://jsonplaceholder.typicode.com/posts/" + id);
  const data = await response.json();
  console.log(data);
  console.log("データを取得完了");
  return;
};

// 非同期で実行するメイン関数
const main = async () => {
  console.log("[main 開始]"); // メイン関数の開始表示

  // データを取得
  await fetchData(1);

  // データを取得
  await fetchData(2);

  console.log("[main 終了]"); // メイン関数の終了表示
};

// mainを実行。呼び先で非同期にサブ関数を呼ぶ。
main();

このコードの実行結果は次のようになります(%はプロンプトです):

% node sample_load_await.js
[main 開始]
データを取得中...
{
  userId: 1,
  id: 1,
  title: 'sunt aut facere repellat provident occaecati excepturi optio reprehenderit',
  body: 'quia et suscipit\n' +
    'suscipit recusandae consequuntur expedita et cum\n' +
    'reprehenderit molestiae ut ut quas totam\n' +
    'nostrum rerum est autem sunt rem eveniet architecto'
}
データを取得完了
データを取得中...
{
  userId: 1,
  id: 2,
  title: 'qui est esse',
  body: 'est rerum tempore vitae\n' +
    'sequi sint nihil reprehenderit dolor beatae ea dolores neque\n' +
    'fugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\n' +
    'qui aperiam non debitis possimus qui neque nisi nulla'
}
データを取得完了
[main 終了]

今度は fetchData() が始まると、 "データを取得中..."、"データを取得完了" と表示され、最後に "[main 終了]" が表示されます。これは、await をつけているため、fetchData の実行が終わるのを待っているからです。
何度実行しても期待通り id 順に表示されてますよね。一方で、順番に処理をしているので、時間がかかってしまいます。

まとめ

  • async 関数を使うときは、処理を同期する場合、await を忘れないようにしましょう。
  • await を忘れると処理順が入れ替わることがあります。用途に応じて使い分けるようにしましょう。

このように、async / await 構文を使えばある程度、非同期、同期を組み合わせた処理を簡潔に書けるようになります。
しかし、関数が増えるとどうでしょう。また、 promise.all や promise.race を駆使するような async / await 処理になると、関数の依存関係が複雑になり、かなり難しいコードになります。
そこでGraphAIの出番になります。
GraphAIは宣言型のプログラミングスタイルで非同期処理を簡潔に記述することができます。
GraphAIのイメージ。

GraphAIの記事
https://zenn.dev/topics/graphai

シンギュラリティ・ソサエティ

Discussion