JavaScript | ゼロからはじめるプログラミング、30日で基礎を学ぶJavaScript:JavaScriptを使えるレベルにする - Day13.5:非同期処理

JavaScript JavaScript
スポンサーリンク

Day13.5 後半のゴール

後半では、Promise / async / await を
「なんとなく書ける」から
「実務で使える形に“設計して”使える」レベルに引き上げます。

特にここを深く押さえます。

Day13.5 後半で意識したいポイント

複数の非同期処理をどう組み合わせるか

async / await と try / catch をセットで使う感覚

失敗・タイムアウト・例外を“前提”にした非同期設計


複数の非同期処理を順番に実行する

then チェーンで書いた場合

まずは Promise だけで「順番に実行する」パターンを見ます。

function fetchUser() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ id: 1, name: "Taro" });
    }, 500);
  });
}

function fetchPosts(userId) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve([
        { id: 1, title: "最初の投稿", userId },
        { id: 2, title: "二つ目の投稿", userId }
      ]);
    }, 500);
  });
}

fetchUser()
  .then((user) => {
    console.log("ユーザー取得:", user);
    return fetchPosts(user.id);
  })
  .then((posts) => {
    console.log("投稿一覧:", posts);
  })
  .catch((error) => {
    console.log("エラー:", error.message);
  });
JavaScript

流れとしては、

ユーザーを取得
→ そのユーザーIDで投稿を取得
→ 結果を表示

という「順番」があります。

then チェーンでも書けますが、
ネストや return の流れを追うのが少し大変です。

async / await で書き直す

同じ処理を async / await で書くとこうなります。

async function main() {
  try {
    const user = await fetchUser();
    console.log("ユーザー取得:", user);

    const posts = await fetchPosts(user.id);
    console.log("投稿一覧:", posts);
  } catch (error) {
    console.log("エラー:", error.message);
  }
}

main();
JavaScript

上から下に、

ユーザーを待つ
→ 投稿を待つ
→ 結果を表示

という順番が、そのまま読めます。

ここで大事なのは、

await する処理を try でまとめて囲んでいる
エラーは catch で一括して扱っている

という構造です。


複数の非同期処理を同時に実行する

Promise.all を使う

「順番に」ではなく「同時に」やりたいこともあります。

例えば、ユーザー情報と通知一覧を同時に取得したいケース。

function fetchUser() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ id: 1, name: "Taro" });
    }, 500);
  });
}

function fetchNotifications() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve([
        { id: 1, text: "お知らせ1" },
        { id: 2, text: "お知らせ2" }
      ]);
    }, 700);
  });
}
JavaScript

これを順番に await すると、
500ms + 700ms で 1200ms かかります。

async function mainSequential() {
  const user = await fetchUser();
  const notifications = await fetchNotifications();
  console.log(user, notifications);
}
JavaScript

同時に走らせるには Promise.all を使います。

async function mainParallel() {
  const [user, notifications] = await Promise.all([
    fetchUser(),
    fetchNotifications()
  ]);

  console.log(user, notifications);
}
JavaScript

ここでは、

fetchUser と fetchNotifications を同時にスタート
両方終わるまで await で待つ
結果が配列で返ってくるので分割代入で受け取る

という流れになっています。

深掘り:どこまで並列にするかは「意味」で決める

順番にやるべきもの(前の結果が次に必要)
同時にやってよいもの(互いに依存していない)

を分けて考えるのが大事です。

ユーザーを取得してから、そのユーザーの投稿を取る
→ 順番が必要なので await を直列に並べる

ユーザー情報と通知一覧を表示したい
→ 互いに独立なので Promise.all で同時に取る

というように、「データの意味」で設計します。


async / await と try / catch の組み合わせを深掘りする

1つのブロックでまとめて扱うパターン

さきほどの main のように、
「この関数の中の await は全部 try で囲う」というパターンはよく使います。

async function main() {
  try {
    const user = await fetchUser();
    const posts = await fetchPosts(user.id);
    console.log(user, posts);
  } catch (error) {
    console.log("エラー:", error.message);
  }
}
JavaScript

この場合、

fetchUser で失敗しても
fetchPosts で失敗しても

どちらも同じ catch に入ります。

「この一連の処理がどこかで失敗したら、こう振る舞う」
という単位で try を置いているイメージです。

await ごとに try を分けるパターン

一方で、「どこで失敗したかによって振る舞いを変えたい」こともあります。

async function main() {
  let user = null;

  try {
    user = await fetchUser();
  } catch (error) {
    console.log("ユーザー取得に失敗:", error.message);
    return;
  }

  try {
    const posts = await fetchPosts(user.id);
    console.log("投稿一覧:", posts);
  } catch (error) {
    console.log("投稿取得に失敗:", error.message);
  }
}
JavaScript

ここでは、

ユーザー取得に失敗したら → そこで処理を終了
投稿取得に失敗したら → ユーザーは表示できるので、投稿だけ諦める

という「強弱」をつけています。

どこを「致命的な失敗」とみなすか
どこは「失敗してもアプリを続けるか」

を決めるのは設計の一部です。


非同期処理とタイムアウト・失敗の設計

「いつまでも待ち続けない」ことも大事

ネットワーク通信は、
サーバーが遅かったり、応答が返ってこなかったりすることがあります。

いつまでも await し続けると、
ユーザーから見ると「固まっている」ように見えてしまいます。

簡易的なタイムアウトの例を見てみます。

function delay(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

async function fetchWithTimeout(promise, ms) {
  const timeout = delay(ms).then(() => {
    throw new Error("タイムアウトしました");
  });

  return Promise.race([promise, timeout]);
}
JavaScript

Promise.race は「どれか1つが先に終わったら、その結果を返す」仕組みです。

これを使って、
「本来の処理」と「タイムアウト用の Promise」を競争させています。

async function main() {
  try {
    const result = await fetchWithTimeout(fetchUser(), 1000);
    console.log("結果:", result);
  } catch (error) {
    console.log("エラー:", error.message);
  }
}

main();
JavaScript

ここでは、

fetchUser が 1秒以内に終わればその結果
1秒を超えたら「タイムアウトしました」というエラー

というふるまいになります。

深掘り:タイムアウト後に「どう振る舞うか」

タイムアウトは「失敗の一種」です。
なので、設計としては、

ユーザーに「時間がかかっています」「再試行してください」と伝える
内部ログに「どのAPIがタイムアウトしたか」を残す
必要なら「再試行ボタン」や「キャンセル」を用意する

といった対応が必要になります。

非同期処理は「成功する前提」ではなく、
「失敗・遅延・タイムアウトが普通に起こる前提」で考えるのがプロの視点です。


セキュリティの視点から見る非同期処理

エラー内容をそのままユーザーに見せない

非同期処理のエラーも、
例外処理と同じく「誰に何を見せるか」が重要です。

async function main() {
  try {
    const data = await fetch("https://example.com/secret");
    console.log(data);
  } catch (error) {
    console.log("読み込みに失敗しました");
    console.error("詳細:", error);
  }
}
JavaScript

ユーザーには「読み込みに失敗しました」とだけ伝え、
内部ログには error をそのまま出す、という分離が基本です。

エラーメッセージやスタックトレースには、
内部構造やURL、ライブラリ名などが含まれることがあり、
攻撃者にヒントを与える可能性があります。

非同期処理の「結果」を信用しすぎない

非同期で取得したデータ(APIレスポンスなど)は、
「信頼できない入力」として扱うべきです。

例えば、

レスポンスの中の role をそのまま信じて管理者扱いにしない
サーバーから返ってきた URL をそのまま画面に埋め込まない
JSON の構造が想定通りかチェックする

など、「外から来たものは疑う」姿勢が大事です。

async / await で書くと、
あたかも「自分の関数の中で作ったデータ」のように見えますが、
中身はあくまで「外部から来たもの」です。


Day13.5 後半のサンプルコード

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Day13.5 非同期処理 後半</title>
  </head>
  <body>
    <h1>Day13.5: 非同期処理(後半)</h1>

    <script>
      function fetchUser() {
        return new Promise((resolve) => {
          setTimeout(() => {
            resolve({ id: 1, name: "Taro" });
          }, 500);
        });
      }

      function fetchNotifications() {
        return new Promise((resolve) => {
          setTimeout(() => {
            resolve([
              { id: 1, text: "お知らせ1" },
              { id: 2, text: "お知らせ2" }
            ]);
          }, 700);
        });
      }

      async function main() {
        try {
          const [user, notifications] = await Promise.all([
            fetchUser(),
            fetchNotifications()
          ]);

          console.log("ユーザー:", user);
          console.log("通知:", notifications);
        } catch (error) {
          console.log("読み込みに失敗しました");
          console.error("詳細:", error);
        }
      }

      main();
    </script>
  </body>
</html>

ユーザー情報と通知を「同時に」取得し、
どちらかが失敗したら「読み込みに失敗しました」と出す、
という実務にかなり近いパターンになっています。


Day13.5 後半のまとめ

Promise は「そのうち結果が入る箱」。
async / await は「その箱を待つ処理を、読みやすく書くための文法」。

後半では、

順番に実行する非同期処理
同時に実行する非同期処理(Promise.all)
async / await と try / catch の組み合わせ
タイムアウトや失敗を前提にした設計
セキュリティ視点でのエラー・データの扱い

まで踏み込みました。

ここまで来ているあなたは、
「非同期が怖い人」ではなく、
「非同期を設計して使える人」に確実に近づいています。

タイトルとURLをコピーしました