404 Not Found

404 Not Found


nginx

JavaScript 非同期プログラミング

JavaScript はシングルスレッドです。一度に1つのことしかできません。しかし、ネットワークリクエストに3秒かかる場合、ページが3秒間フリーズするのは困りますよね?非同期プログラミングがこの問題を解決します。待つ代わりに番号札を受け取り、準備ができたら戻ってきます。レストランの行列システムのようなもので、キッチンのドアの前で立つのではなく、座って番号が呼ばれるのを待ちます。


同期 vs 非同期

同期:コードは行ごとに実行されます。次の行は現在の行が完了するまで待ちます。列に並ぶようなもので、前の人が終わるまで待ちます。

非同期:特定の操作(ネットワークリクエスト、タイマー、ファイル I/O)は後続のコードをブロックせずに開始されます。完了時にコールバックや Promise で通知を受けます。

HTML
<script>
console.log('1');
setTimeout(function() {
  console.log('2');
}, 1000);
console.log('3');
</script>

出力順は 1 → 3 → 2 です。setTimeout は非同期で、そのコールバックは1秒後に console.log('3') をブロックせずに実行されます。


コールバック

コールバックは最も基本的な非同期パターンです。関数を引数として渡し、非同期操作が完了した時に呼び出されます。

例:基本的なコールバックの使用

HTML
<div id="output" style="padding: 10px; border: 1px solid #ccc;"></div>

<script>
const output = document.getElementById('output');

function fetchData(callback) {
  output.textContent = '読み込み中...';
  setTimeout(function() {
    callback('データ読み込み完了!');
  }, 2000);
}

fetchData(function(data) {
  output.textContent = data;
});
</script>
▶ 試してみよう

コールバック地獄

非同期操作が順番に依存し合うと、コールバックが深くネストされます。これが悪名高い「コールバック地獄」です。

HTML
<script>
getUser(function(user) {
  getOrders(user.id, function(orders) {
    getOrderDetail(orders[0].id, function(detail) {
      getItems(detail.itemId, function(items) {
        console.log('4レベルの深さ。さらに複雑になると読みにくくなる');
      });
    });
  });
});
</script>
💡 コールバック地獄は「水平ピラミッド」のような形で、読みにくくメンテナンスが困難です。Promise はこの問題を解決するために発明されました。


Promise

Promise は非同期操作の最終結果を表すオブジェクトです。3つの状態があります。

状態 説明
pending 進行中。結果はまだない
fulfilled 正常に完了。結果が利用可能
rejected 失敗。エラーが利用可能

Promise が pending から fulfilled または rejected に遷移すると、二度と元に戻りません。射た矢は召回できないようなものです。

Promise の作成

HTML
<script>
const promise = new Promise(function(resolve, reject) {
  // 非同期操作
  // 成功時は resolve(result) を呼び出す
  // 失敗時は reject(error) を呼び出す
});
</script>

then / catch / finally

HTML
<script>
promise
  .then(function(result) { console.log(result); })   // 成功時に実行
  .catch(function(error) { console.error(error); })    // 失敗時に実行
  .finally(function() { console.log('完了'); });       // 常に実行
</script>

例:Promise でリクエストをシミュレート

HTML
<button id="loadBtn">データ読み込み</button>
<div id="output" style="padding: 10px; border: 1px solid #ccc; margin-top: 10px;"></div>

<script>
const btn = document.getElementById('loadBtn');
const output = document.getElementById('output');

function fetchUser() {
  return new Promise(function(resolve, reject) {
    output.textContent = '読み込み中...';
    setTimeout(function() {
      const success = true;
      if (success) {
        resolve({ name: 'Alice', age: 25 });
      } else {
        reject(new Error('読み込み失敗'));
      }
    }, 1500);
  });
}

btn.addEventListener('click', function() {
  fetchUser()
    .then(function(user) {
      output.textContent = '名前: ' + user.name + ', 年齢: ' + user.age;
    })
    .catch(function(err) {
      output.textContent = 'エラー: ' + err.message;
    })
    .finally(function() {
      console.log('リクエスト完了(成功または失敗)');
    });
});
</script>
▶ 試してみよう

チェーン

then 自体が Promise を返すため、呼び出しをチェーンできます。コールバック地獄が直線になります。

HTML
<script>
fetchUser()
  .then(function(user) { return fetchOrders(user.id); })
  .then(function(orders) { return fetchDetail(orders[0].id); })
  .then(function(detail) { console.log(detail); })
  .catch(function(err) { console.error(err); });
</script>
💡 チェーンの最後に1つの catch を置くと、任意のステップのエラーをキャッチします。各 then の後に catch を置く必要はありません。


async / await

async/await は Promise のシンタシュガーです。非同期コードを同期的に見せかけ、可読性を大幅に向上させます。

例:上記のリクエストを async/await で書き直す

HTML
<button id="loadBtn">データ読み込み</button>
<div id="output" style="padding: 10px; border: 1px solid #ccc; margin-top: 10px;"></div>

<script>
const btn = document.getElementById('loadBtn');
const output = document.getElementById('output');

function fetchUser() {
  return new Promise(function(resolve) {
    setTimeout(function() {
      resolve({ name: 'Bob', age: 30 });
    }, 1500);
  });
}

btn.addEventListener('click', async function() {
  try {
    output.textContent = '読み込み中...';
    const user = await fetchUser();
    output.textContent = '名前: ' + user.name + ', 年齢: ' + user.age;
  } catch (err) {
    output.textContent = 'エラー: ' + err.message;
  } finally {
    console.log('リクエスト完了');
  }
});
</script>
▶ 試してみよう
💡 コールバックと比較して、async/await は「番号札をもらって待つ」を「並んでそのまま待つ」に変えます。コードフローが直線的になり、可読性が大幅に向上します。

エラー処理

async/awaittry/catch でエラー処理を行います。同期コードと同じパターンです。

HTML
<script>
async function loadAll() {
  try {
    const user = await fetchUser();
    const orders = await fetchOrders(user.id);
    console.log(orders);
  } catch (err) {
    console.error('何か問題がありました:', err);
  }
}
</script>

Promise.all と Promise.race

メソッド 説明
Promise.all([p1, p2, p3]) すべてが成功した場合のみ成功。1つが失敗すると即座に失敗
Promise.race([p1, p2, p3]) 最初に完了したもの(成功または失敗)の結果を使用

例:Promise.all による並行リクエスト

HTML
<button id="loadAll">並行読み込み</button>
<div id="output" style="padding: 10px; border: 1px solid #ccc; margin-top: 10px;"></div>

<script>
const btn = document.getElementById('loadAll');
const output = document.getElementById('output');

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

btn.addEventListener('click', async function() {
  output.textContent = '読み込み中...';

  const results = await Promise.all([
    delay(1000, 'ユーザーデータ'),
    delay(1500, '注文データ'),
    delay(800, '設定データ')
  ]);

  output.textContent = 'すべて読み込み完了:\n' + results.join(' | ');
});

const raceBtn = document.createElement('button');
raceBtn.textContent = 'レース読み込み';
document.body.appendChild(raceBtn);

raceBtn.addEventListener('click', async function() {
  const fastest = await Promise.race([
    delay(1000, 'ソース A(1秒)'),
    delay(500, 'ソース B(0.5秒)'),
    delay(800, 'ソース C(0.8秒)')
  ]);
  output.textContent = '最速: ' + fastest;
});
</script>
▶ 試してみよう
💡 Promise.all はグループディナーのようなもの。全員が到着するまで食べられません。Promise.race はレースのようなもの。最初にゴールした人が勝ちです。独立したリクエストの高速化には all を、複数のミラーソースから最速のものを得るには race を使いましょう。


📖 まとめ

  1. 同期コードは後続の実行をブロックするが、非同期コードはブロックしない。シングルスレッドでも「複数のことができる」
  2. コールバックは最も基本的な非同期パターン。深いネストはコールバック地獄を生む
  3. Promise には3つの状態:pending → fulfilled または pending → rejected。状態遷移は不可逆
  4. then/catch/finally は Promise を消費する。チェーンはコールバック地獄を解決
  5. async/await は Promise のシンタシュガー。awaitasync 関数内でのみ使用可能
  6. Promise.all はすべての成功を待つ。Promise.race は最初に完了したものを取る

❓ よくある質問

Q 通常の関数で await は使えますか?
A いいえ。awaitasync 関数内でのみ使用できます。通常の関数で await を使うと構文エラーになります。トップレベルの await はモジュールでサポートされていますが、ブラウザのサポート状況は異なります。
Q Promise.all のいずれかの Promise が失敗するとどうなりますか?
A Promise.all は最初のエラーで即座に拒否されます。他の Promise はまだ実行されますが、その結果は無視されます。「結果に関係なくすべて完了する」必要がある場合は、Promise.allSettled を使いましょう。
Q async 関数は何を返しますか?
A async 関数は常に Promise を返します。return 42 としても、実際には Promise.resolve(42) を返します。そのため、.then() で結果を消費できます。

📝 演習

  1. 基礎Promise + setTimeout を使って2秒の遅延をシミュレートし、成功時に「完了」と出力してください。
  2. 中級:上記を async/await で書き直し、try/catch のエラー処理を追加してください。
  3. チャレンジPromise.all を使って、異なる遅延(1秒/2秒/3秒)の3つのシミュレートリクエストを同時に発火し、すべて完了した時の合計経過時間を計算して表示してください。
100%