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?

【JavaScript】なんとなくawaitからの脱却

Last updated at Posted at 2025-07-27

はじめに

JavaScriptにおいて欠かせない非同期処理ですが、なんとなくここで await しとこう、みたいになってませんか?
改めてイチから丁寧に理解し直したいと思い、ゆっくりまとめてみることにしました。
この記事もまた完全に私的備忘録ですが、参考になれば幸いです!

※誤りありましたら、ご指摘いただけますと幸いです。

そもそも、非同期処理はなぜ有効か

本題に入る前に、まずそもそも非同期であることのメリットから理解してみましょう。
大前提として、JavaScriptはシングルスレッドで動作する言語です。
これは、同時に一つの処理しか実行できないということを意味します。

例えば膨大なデータを取得することで時間がかかる処理や、ファイルの読み込み処理等、これらの処理も、
シングルスレッドである以上、同期的に処理をすると、処理が詰まってしまいます。

ここで非同期処理の出番です。
処理を非同期的に扱うことで、それらの処理の待ち時間に他の処理を進める ことができるようになります。

アプリケーションにおいて、ロードに時間がかかり、待たされるのってストレスですよね?
非同期処理であれば、UIの応答性を保ちながら、背後でAPI通信等の重い処理を進め、終わったタイミングで画面に反映する、
こんなこともできるわけです。

でもそれって、並行処理をしているという事では?

確かに、便宜上イメージしやすいように、あたかも背後で処理を進める、という書き方をしましたが、実際は異なります。
ここで新しい書き方をしますが、非同期処理 = ノンブロッキング処理という点を理解する必要があります。

先述の通り、JavaScriptはシングルスレッドですから、処理の流れは1本道(以降メインスレッドと書きます)しかありません。
そのため、基本的には(イメージしやすいようにあえていうならば)上から下に処理が進んでいきます。

例えば下の例。

  console.log('A');
  console.log('B');
  console.log('C');

これを出力すると、「A → B → C」という順でコンソール上に出力されます。
上から下に処理が流れていますね。
これが、同期的な処理です。

もう一方で下の例を見てみましょう。

  console.log('A');

  setTimeout(() => {
      console.log('B');
  }, 0);

  console.log('C');

これを出力すると、「A → C → B」という順で出力されます。
これは、setTimeoutメソッドが、非同期処理を行うからです。

先程紹介した同期的な処理の場合だと、Aを出力する処理 → Bを出力する処理 → Cを出力する処理という順でメインスレッドで順に実行されます。
一方非同期的な処理の場合、setTimeoutメソッドに引数として渡されたBを出力する処理(コールバック)は、一時的に外に預けられ、実行は後回しになります。
(ITパスポートや基本情報を学んだ方だったら、キューに入るといった方が伝わりやすいかもしれません)

実行されるのは、メインスレッドが空いたタイミングで、上記の例で言うと、一番下の処理であるCを出力する処理が終わったタイミングです。
このタイミングで外に預けていたBを出力する処理を取りに行き(キューから取り出されて)実行されるため、Bの出力が一番最後だった、というわけです。

少し長くなりましたが、厳密に言うと、非同期処理は決して並行実行しているわけではないですが、もちろん高速実行されますから、同時実行しているように見えているわけですね。

Promiseのおさらい

async/awaitの解説の前に、Promiseオブジェクトについても触れておきましょう。
Promiseオブジェクトは、「まだ終わっていない処理の結果(非同期処理の結果)」を、将来受け取ることを約束するオブジェクトです。

そのため、Promiseオブジェクトは、3つの状態を持ちます。

状態 説明
pending(保留) 処理中で、まだ完了も失敗もしていない状態
fulfilled(成功) 非同期処理が成功し、結果が返ってきた状態
rejected(失敗) 非同期処理が失敗し、エラーが返ってきた状態

基本構文

上記の状態を踏まえた上で、下記の構文を見てみましょう。

  const promise = new Promise((resolve, reject) => {
      const success = true;

      if (success) {
          resolve('処理が成功しました');
      } else {
          reject('エラーが発生しました');
      }
  });

  promise
      .then(result => {
          console.log(result); // 成功時のメッセージ
      })
      .catch(error => {
          console.error(error); // エラー時のメッセージ
  });

さて、このコードのポイントは、Promiseオブジェクト内の関数(executor関数と呼びます)内で、
resolve()もしくはreject()が呼ばれるタイミングで、Promiseの状態が確定する、ということです。

ちなみに、executor関数は同期的に呼ばれます。

そして、コメントにもあるように、resolve()が呼ばれて成功した場合は、 .then() の処理に進み、
reject()が呼ばれて失敗した場合は、 .catch() の処理に進みます。

なお、ここで大事なのは、executor関数が同期的に即実行される(new Promise()の時点で)のに対し、.then()や.catch()の処理は非同期的に実行される(メインスレッドが空くまで待機される)ということです。
つまり、下記のコードの場合、

  console.log("1");

  const promise = new Promise((resolve, reject) => {
      // ここは同期的に実行
      console.log("2");
      resolve("成功");
  });

  promise.then((result) => {
      // ここは非同期的に実行
      console.log("3:", result);  
  });

  console.log("4");

出力される順番は、「1 → 2 → 4 → 3:成功」という順番になります。

async/awaitの基本

さて、それではここから本題のasync/awaitの処理の説明に入ります。
JavaScriptの非同期処理において、Promiseは便利でしたが、処理の流れが断片的になりがちで、
複雑な処理においては可読性が落ちてしまうというデメリットがありました。(どういうことか後で解説します)

そこで登場したのが、async/awaitです。
これにより、Promiseの書き方を直感的・かつ同期的に書くことができるようになりました。

実際の構文を見てみましょう。
下記はAPIを叩いてデータを取得する処理です。

  async function getData() {
      const response = await fetch('https://example.com/data');
      const data = await response.json();
      console.log(data);
  }

このコードは、見た目はまるで同期処理のようですが、実際は非同期処理です。

まず、関数宣言 function の前でasync を宣言しています。
ちなみにasyncは非同期的な、という形容詞である asynchronous という英単語が由来です。
async を付けることで、その関数は常にPromiseを返す関数 になります。

続いて、関数内、変数 response と data で await を使用しています。
awaitは「待つ」という意味の英語の通り、Promiseの完了(成功 or 失敗)を待つためのキーワードになります。

上記の例で言うと、awaitを使うことで、response で fetch しているデータが返ってくるのを待機し、
そして、data でレスポンスボディ(中身)を JSON としてパース(変換)する処理を行っていますね。

それらの完了を待ってから、data がコンソールに出力される、というわけです。

可読性が上がるとは

では、async/awaitを使用すると、可読性はどのように向上するのでしょうか。
上記の例だと Promise と比較して目立った可読性の向上は見られない気もします。

では、次のパターンはどうでしょうか。
少し雑ではありますが、

  1. ユーザーIDからユーザー情報を取得
  2. ユーザー情報から投稿一覧を取得
  3. 各投稿の最初のコメントを取得
    というケースのコードを比較してみましょう。

まずは Promise のパターン。

  getUserById(1)
  .then(user => {
      return getPostsByUser(user.id);
  })
  .then(posts => {
      return getFirstComment(posts[0].id);
  })
  .then(comment => {
      console.log("最初のコメント:", comment);
  })
  .catch(error => {
      console.error("エラー:", error);
  });

.then() を使用したチェーンで処理を書けはするものの、次に何が起こるかが .then() の中に隠れていて少し追いづらい感じがします。

続いて、async/await のパターン。

  async function displayFirstComment() {
      try {
          const user = await getUserById(1);
          const posts = await getPostsByUser(user.id);
          const comment = await getFirstComment(posts[0].id);

          console.log("最初のコメント:", comment);
      } catch (error) {
          console.error("エラー:", error);
      }
  }

  displayFirstComment();

いかがでしょうか。
Promiseと比べて、同期処理のような見た目で非同期処理を順番に書けるため、処理の流れが追いやすいのではないでしょうか。
個人的にも流れは async/await の方が追いやすいので、こちらを使用することが多いです。

try/catchでのエラーハンドリング

既に上述の例で示されていますが、async/await で書く上での大きな魅力の一つとして、try/catchで例外処理を書ける ことが挙げられます。

では、Promiseの場合はどのように書くかというと、下記のように .catch を使用します。

  fetch('https://api.example.com/user')
    .then(res => res.json())
    .then(data => console.log(data))
    .catch(error => console.error('エラー:', error));

.then() のチェーンに続く形で、.catch を使用することで、エラーの捕捉が可能です。
ですが、やはり同期処理のように馴染みのある try/catch で書けるという点は、async/await を採用する一つのメリットでもあります。

ちなみに、try/catch を使用できるということは、finally も使用できるということになります。
→コメントで指摘いただきました。promiseの場合であっても、then~catch~finallyに形で使用可能です。失礼いたしました。
MDN-Promise.prototype.finally()

並列処理と高速化

まずは処理の高速化について。
これまで、awaitは逐次処理として一つずつ順番に記述してきました。
もちろん、取得結果を次の処理に使いたい、という事であれば致し方ありませんが、そうでない場合は、無駄に時間がかかってしまうだけです。

そこでオススメなのが Promise.all を使用した処理です。
下記を見てみましょう。

  const [data1, data2] = await Promise.all([
    fetch('/api/data1'),
    fetch('/api/data2'),
  ]);

Promise.allの最大の特徴は、全てのPromiseが成功(fulfilled)した場合のみ .then()に進むという点でしょう。
また、1つでも失敗した場合は即座に .catch()に飛ぶということにもなります。

という点は、取り上げられることも多いのでご存じの方も多いかと思いますが、ここで注目したいのが、
Promise.allの引数に与えられた配列内の処理(例で言うと fetch )は、同時に開始されるという点です。

例えば上記の例で言うところの、1つ目の fetch に3秒、2つめの fetch に2秒かかるとした場合、
逐次処理の場合は合計5秒かかる計算になります。

一方で、Promise.all の場合は、3秒係る処理と2秒かかる処理が同時に開始しますから、
合計3秒しかかかりません。(2秒かかる処理の方は先に終わります)

このように、無関係である処理は並列処理をした方が、高速化が期待できます。

【注意】ループ内での await の使用について

ループで await を使用したい場合、 forEach自体は Promise の完了を待たないため、非同期処理には不向きであるため、
期待する挙動にはなりません。
下記を見てみましょう。

  const urls = ['/a', '/b', '/c'];

  urls.forEach(async (url) => {
    const res = await fetch(url); // ここにawaitがあるが
    const data = await res.json();
    console.log(data);
  });

  console.log('全て完了!'); // ← 先に実行される可能性が高い

コメントでも書いた通りですが、forEach で await を記述した場合、
期待するような Promise の完了を待つ挙動にはなりません。
ループ処理 async/await を使用したい場合は、forEachではなくfor~ofを使用するようにしましょう。

  const urls = ['/a', '/b', '/c'];

  for (const url of urls) {
    const res = await fetch(url);     // ちゃんと待つ!
    const data = await res.json();
    console.log(data);
  }

  console.log('全て完了!'); // ← fetch完了後に実行される

実践:簡単なAPIクライアントを作成してみる

さて、最後にこれまでの総まとめとして、実際に API を叩き、async/await を使用してみたいと思います。

今回は、学習用に使えるダミーAPIサービスである、JSONPlaceholderを使い、
ダミーのユーザデータを取得、コンソール出力までを実装してみます。

早速ですが、実装した結果が下記です。

  async function fetchUsersAndPosts() {
    try {
      // 1. ユーザー一覧を取得
      const userRes = await fetch('https://jsonplaceholder.typicode.com/users');

      if (!userRes.ok) throw new Error('ユーザー取得に失敗しました');

      const users = await userRes.json();
      console.log('ユーザー一覧:', users);

      // 2. 最初のユーザーを取得
      const firstUser = users[0];
      console.log(`最初のユーザー: ${firstUser.name}(ID: ${firstUser.id})`);

      // 3. そのユーザーの投稿を取得
      const postRes = await fetch(`https://jsonplaceholder.typicode.com/posts?userId=${firstUser.id}`);

      if (!postRes.ok) throw new Error('投稿取得に失敗しました');

      const posts = await postRes.json();

      console.log(`\n${firstUser.name}さんの投稿一覧:`);
      posts.forEach(post => {
        console.log(`- ${post.title}`);
      });

    } catch (error) {
      console.error('エラーが発生しました:', error);
    } finally {
      console.log('\n📘 処理完了');
    }
  }

  fetchUsersAndPosts();

fetch での取得結果等はPostman等で実際に確認しながら進めていただければと思うのですが、
ざっくり、ここでは async関数 内で API を叩き、都度その後に使用したい情報を含む場合は、await を使用しているのが見てとれると思います。

いかがでしょうか。実際の使用方法はイメージできたでしょうか。
先述のように、なんでもかんでも await を使用すると処理の低速化につながりますから、そこには注意するようにしましょう。

まとめ

非同期処理について、JavaScriptを使用する中で頻繁に使いはするものの、正直付け焼刃感があったというか、
丁寧に一回まとめてみたいな、と思ってずるずるここまで先延ばしてきたので、改めてこのタイミングでまとめてみました。

最近読んだエンジニアリング関連の書籍にも、 「なんとなく分かってるを放置しない」 ことの重要性が語られており、基礎を猛復習中でございます。

非同期処理はその裏側、Web API やマイクロタスク、マクロタスク等、もっと深い部分も知りたい分野ですので、ここも勉強したいと思いました。

ご覧いただきありがとうございました。

0
1
2

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?