JavaScript

JavaScriptの同期・非同期処理の違い|Promiseとasync/awaitの使い分け

同期処理は完了してから次へ進み、非同期処理は完了をPromiseやcallbackで後から受け取ります。逐次・並行実行、async/await、fetchの失敗処理、キャンセルまで実例で解説します。

この記事の目次
  1. 同期と非同期は何が違う?
  2. Promiseは何を表す?
  3. async/awaitはどう使う?
  4. 逐次実行と並行開始はどう選ぶ?
  5. 非同期処理をキャンセルするには?
  6. よくある失敗はどう直す?
  7. 実装手順をどう組み立てる?

同期処理は、その処理が完了して結果が返るまで次の文へ進みません。非同期処理は時間のかかる操作を開始し、完了結果をcallbackやPromiseとして後から受け取ります。network、timer、user inputの待ちには非同期APIを使い、結果に依存する部分だけをawaitします。ただしasync functionへ変えるだけで重い計算が軽くなったり、処理が自動でparallelになったりはしません。

同期と非同期は何が違う?

違いは「結果をいつ受け取るか」と「待つ間に呼び出し元が進めるか」です。同期functionは通常のreturn値を返します。Promise-basedな非同期functionは、将来の成功値または失敗理由を表すPromiseをすぐ返します。

項目 同期 非同期
結果 呼び出し中にreturnする Promiseやcallbackで後から受け取る
向く処理 短い計算、即時の値変換 network、timer、user操作、非同期storage
error throwをtry/catchする Promise rejectionをawaitとtry/catch等で扱う
注意 長い処理はmain threadを塞ぐ 順序、取消、重複実行を設計する

AJAXは歴史的に、page全体をreloadせずserverと非同期通信して表示を更新するWeb開発手法を指します。現在はXMLHttpRequestだけでなくFetch APIとJSONを使う構成が一般的です。「AJAX」という別の実行モデルがあるのではなく、HTTP通信を非同期に扱う利用例です。XMLHttpRequestとPromiseの書き方の違いはJavaScriptのXMLHttpRequestとPromiseの違いで比較しています。

「Web API」という言葉は、この文脈ではbrowserが提供する機能(fetch、setTimeout、DOM eventなど)を指します。JavaScriptの言語仕様そのものには時間を待つ仕組みがなく、待ち時間の処理はこれらのbrowser側のAPIが引き受け、完了後にcallbackやPromiseの結果としてJavaScriptへ戻します。

スポンサーリンク

Promiseは何を表す?

Promiseは非同期操作の最終的な完了または失敗を表すobjectです。pendingからfulfilledまたはrejectedへsettleし、一度settleした状態は変わりません。Promiseの中身を同期的に取り出すことはできず、thencatchfinallyまたはawaitで後続処理をつなぎます。

function wait(milliseconds) {
  return new Promise((resolve) => {
    setTimeout(resolve, milliseconds);
  });
}

wait(500)
  .then(() => {
    console.log("完了");
  })
  .catch((error) => {
    console.error(error);
  });

Promise constructorで包むのはcallback APIをPromise化するときに必要です。すでにPromiseを返すfetchなどを新しいPromiseで二重に包む必要はありません。setTimeoutとsetIntervalそのものの挙動はsetIntervalとsetTimeoutの使い方で扱っています。

async/awaitはどう使う?

async functionは必ずPromiseを返します。awaitは周囲のasync functionの続きだけをPromiseのsettleまで保留し、fulfilled値を取り出すか、rejectionをthrowとして扱います。

async function loadUser(userId, signal) {
  const response = await fetch("/api/users/" + userId, { signal });

  if (!response.ok) {
    throw new Error("ユーザー取得に失敗しました: " + response.status);
  }

  return response.json();
}

fetchはnetwork errorやabortではrejectしますが、404や500などHTTP error statusだけでは通常rejectしません。そのためresponse.okまたはstatusを確認して、自分で失敗として扱います。JSONの構造も外部入力なので、必要なfieldと型を別に検証してください。

逐次実行と並行開始はどう選ぶ?

前の結果が次の入力になるなら逐次実行します。独立した複数操作なら先にPromiseを作り、まとめて待つと総待ち時間を減らせます。ここでいう並行開始は、JavaScriptのCPU計算が同じthreadでparallelになるという意味ではありません。

// 逐次: userIdが分かってからpostsを取得する
const user = await fetchUser();
const posts = await fetchPosts(user.id);

// 独立: profileとsettingsを先に開始する
const [profile, settings] = await Promise.all([
  fetchProfile(),
  fetchSettings(),
]);
組み合わせ 完了条件 向く場面
順番にawait 各処理を1つずつ待つ 依存関係がある、負荷を順にかけたい
Promise.all() 全成功。1つrejectすると結果もreject 全結果が必要な独立処理
Promise.allSettled() 全Promiseがsettle 一部失敗も含めて全結果を確認する
Promise.race() 最初にsettleしたもの 明確なrace semanticsが必要な場合
Promise.any() 最初のfulfilled 複数候補のどれか1つ成功すればよい場合

同時に大量requestを開始するとserverやbrowserへ負荷をかけます。件数が多い場合はconcurrency上限を設け、retryと重複送信の影響を確認します。

非同期処理をキャンセルするには?

Promise一般に共通のcancel methodはありません。Fetch APIなど対応するAPIではAbortControllerのsignalを渡します。画面遷移、検索語の更新、component破棄など、結果が不要になった時点でabortします。

const controller = new AbortController();

try {
  const user = await loadUser("42", controller.signal);
  console.log(user);
} catch (error) {
  if (error.name === "AbortError") {
    console.log("取得を中止しました");
  } else {
    throw error;
  }
}

// 必要がなくなった時点で呼ぶ
controller.abort();

abortしたこととserver側の処理が必ず取り消されたことは同義ではありません。既にserverへ届いた更新requestは処理される場合があります。重要な更新ではidempotencyや重複防止をserver側でも設計します。

よくある失敗はどう直す?

症状 原因候補 戻し方・対策
Promiseが画面に表示される 結果をawaitせずそのまま使った async contextでawaitし、loading/error状態も用意する
errorがcatchされない Promiseをreturn・awaitしていない chainを返し、最上位でrejectionを処理する
404なのに成功処理へ進む fetchのresponse.ok未確認 bodyを読む前にstatusを判定する
古い検索結果で上書きされる 複数requestの完了順が入れ替わる 前requestをabortするかrequest IDで最新だけ反映する
UIが固まる await前後に重い同期計算がある algorithmを改善し、必要ならworkerへ分離する

順序問題を一定時間のsetTimeoutで隠すと、通信速度が変わったとき再発します。完了条件をPromiseとして表すか、stateを識別するIDを持たせます。event loopのtaskとmicrotaskまで追う必要がある場合はJavaScriptのevent loopを確認してください。

実装手順をどう組み立てる?

  1. 待ちが必要な境界を特定し、Promiseを返すAPIか確認する
  2. 成功値、HTTP error、network error、abortを分ける
  3. 処理間の依存関係を図にし、逐次か独立か決める
  4. loading中の二重実行と、結果不要時のcancelを設計する
  5. 成功・失敗・遅延・順序逆転のtestを行う

まず1つのfetchをtry/catchで閉じ、response.ok、JSON検証、画面のloading/errorを確認します。その後で独立requestをPromise.allへまとめます。APIごとの実装はFetch APIでJSONを取得する方法、async/awaitの文法とerror伝播はasync/awaitの解説へ進んでください。非同期化は速さを保証する機能ではなく、待ちと依存関係を明示する設計です。

関連する内容は、JavaScript async/awaitの使い方|エラー処理と並行実行JavaScriptのイベントループ入門|task・microtask・実行順を理解するもあわせて確認してください。

スポンサーリンク