JavaScriptの非同期処理入門:同期と非同期、Promise、async/await、Fetchまでまとめて理解する

10 min 72 views
Javascript-promise

Webサイトは、もう「ただ開いて読むだけ」の時代ではありません。
ボタンを押すと最新ニュースが読み込まれる、ページ遷移なしでエラーメッセージが表示される、位置情報や天気をサッと取ってきて画面に出す──。
こういった”止まらないUI”はすべて「非同期処理」という仕組みで動いています。

この記事では、はじめての人でもついていけるように、

  1. 同期処理ってなに?
  2. 非同期処理ってなに?
  3. Promiseは何をしてくれるの?
  4. async / await がなぜ読みやすいの?
  5. そして実戦編:Fetch APIでサーバーからデータ(JSON)をもらう

まで一気に整理していきます。

「難しそう…」って感じていても大丈夫。
順番に読めば「なんで必要なのか」「どこで使うのか」までちゃんとイメージできるようにしていきます。

1. 同期処理(synchronous)とは?まずはここから

同期処理とは、「上から順番に、1つずつ終わらせてから次に進む」やり方のことです。

イメージでいうと、コンロが1口しかないキッチンで料理している状態です。

  1. まずソースを作る
  2. ソースが終わるまでハンバーグを焼けない
  3. ソースが完成してから、ようやくハンバーグを焼く
  4. 最後にソースをかけて完成

同時に複数のことはできないから、常に「待ち」が発生しますよね。これが同期処理です。

プログラム的に言うと:

  • 今この行を実行中 → 終わるまで次の行には進まない
  • 関数を呼び出した → その関数の処理が終わるまで、呼び出し元はいったんストップ

という流れになります。

 同期処理のサンプルコード

function logTaskA() {
  const now = new Date();
  console.log(`TaskAスタート:${now.toLocaleString()}`);
}

function logTaskB() {
  const now = new Date();
  console.log(`TaskBスタート:${now.toLocaleString()}`);
}

// CPUをわざとつかませて「待たせる」処理
function busyBlock(waitMs) {
  const begin = Date.now();
  while (true) {
    const elapsed = Date.now() - begin;
    if (elapsed >= waitMs) {
      return; // 指定ミリ秒経ったら抜ける
    }
  }
}

logTaskA();        // 1. まずAを実行
busyBlock(2000);   // 2. 強制的に2秒ブロック
logTaskB();        // 3. 2秒後にBが実行される

流れはこうです:

  1. logTaskA() がすぐ動く
  2. busyBlock(2000) が2秒間ブロックして止める
  3. 2秒経つまで logTaskB() は実行されない

つまり「A→待つ→B」という順番が必ず守られます。これが同期処理。

2. 非同期処理(asynchronous)とは?「待ってる間に別のことする」

では非同期処理は何が違うのか。

コンロが2口あるキッチンを想像してください。

  • コンロ1でソースを煮込みながら
  • コンロ2で同時にハンバーグを焼ける

「ソースが終わるまで何もしない」じゃなくて、「待ってる間に別の仕事を進める」ことができます。
これが非同期処理のイメージです。

プログラム的に言うと:

  • 「〇秒後にこれ動かしておいて」と予約だけして、すぐ次の処理に進む
  • その”予約された処理”は、あとで別タイミングで実行される
  • メインの流れは止まらない

なので、ユーザーは「画面が固まってる…」と感じにくくなります。UX(使いやすさ)が上がるんです。

3. 非同期処理の実例:setTimeout

JavaScriptには、すぐに使える代表的な非同期APIとして setTimeout があります。

setTimeout(コールバック関数, 待ち時間ミリ秒)
という形で使います。
「待ち時間ミリ秒が経ったら、このコールバック関数を呼んでね」という予約だけして、呼び出し元は止まらずに次に進みます。

 同期版を非同期版に書き直す

さきほどの同期処理コードを、わざと非同期っぽく書き換えるとこうなります。

function logTaskA() {
  const now = new Date();
  console.log(`TaskAスタート:${now.toLocaleString()}`);
}

function logTaskB() {
  const now = new Date();
  console.log(`TaskBスタート:${now.toLocaleString()}`);
}

function logAsyncWork() {
  const now = new Date();
  console.log(`あとから実行された処理:${now.toLocaleString()}`);
}

logTaskA();

// 「2秒後にlogAsyncWorkを呼び出してね」と予約だけする
setTimeout(() => {
  logAsyncWork();
}, 2000);

logTaskB();

これを実行すると、だいたいこんな順番になります。

  1. TaskAスタート(すぐ出る)
  2. TaskBスタート(Aのすぐ後に出る)
  3. 2秒たってから あとから実行された処理

つまり、setTimeout の部分はすぐには実行されないんです。
「タイマーを仕込んでから、先にBを進める」という動きになる=非同期。

これが「待ってる間に別のことをする」という感覚です。

4. でも非同期はこれで終わらない:Promiseという考え方

setTimeout だけで済むうちはまだいいのですが、現実のWeb開発では「サーバーからデータを取ってくる」「通信が成功したらUIを更新する」「失敗したらエラーメッセージを出す」といったパターンが必須になります。

そこで登場するのが Promise(プロミス)です。

 Promiseはなにを約束してくれる?

Promiseは「非同期の結果(成功 or 失敗)を、あとから教えてくれる箱」です。

  • 成功したら → resolve(...) を呼ぶ
  • 失敗したら → reject(...) を呼ぶ
  • 呼び出し側は .then(...) で成功時の処理を、.catch(...) で失敗時の処理を書ける

これによって、「いつ終わるかわからない処理」に対して、”終わった後に何をするか”をきれいに書けます。

 Promiseの基本形

// 非同期で何かしたい処理をPromiseとして返す関数
function runAsyncDemo() {
  return new Promise((ok, ng) => {
    // ここに非同期の処理を書くイメージ
    const isSuccess = false; // 成功と失敗を切り替えるテスト用フラグ

    if (isSuccess) {
      ok("非同期処理は成功しました!");
    } else {
      ng("非同期処理でエラーが発生しました。");
    }
  });
}

// Promiseを使う側
runAsyncDemo()
  .then((msg) => {
    console.log("成功時のメッセージ:", msg);
  })
  .catch((errMsg) => {
    console.error("失敗時のメッセージ:", errMsg);
  });

ここで大事なのは、

  • runAsyncDemo() を呼んだ瞬間に結果が返るわけじゃない
  • .then(...) の中は「処理が終わったあと」に呼ばれる
  • .catch(...) には失敗パターンがくる
    ということです。

この「終わったあとでやることを登録しておく」という感覚がPromiseの世界観です。

5. Promiseを使って遅延処理を書く(setTimeout版のPromise化)

さっきの「2秒後に実行する処理」をPromise化してみると、もっとイメージしやすいです。

// ログ用の共通メッセージを返すだけの関数
function buildStartLog(label) {
  const now = new Date();
  return `${label} 開始時刻:${now.toLocaleString()}`;
}

// ただの処理A
function showTaskA() {
  console.log(buildStartLog("処理A"));
}

// ただの処理B
function showTaskB() {
  console.log(buildStartLog("処理B"));
}

// 2秒後にメッセージを返すPromise
function waitAndReport() {
  return new Promise((done) => {
    setTimeout(() => {
      // 2秒後に完了扱いにして値を返す
      done(buildStartLog("遅延タスク"));
    }, 2000);
  });
}

showTaskA();

// Promiseの完了後(then)にログを出す
waitAndReport().then((message) => {
  console.log(message);
});

showTaskB();

この場合の実行イメージはこうです:

  1. 「処理A」ログが出る
  2. すぐ「処理B」ログも出る
  3. 約2秒後に「遅延タスク」のログが出る

waitAndReport() はすぐには結果を返さず、「あとで結果を渡すからね」というPromise(約束)を返しています。
呼び出し側は .then(...) の中に「結果が来たらやること」を書けばOK。

6. async / await:Promiseをもっと読みやすくする書き方

Promiseは便利なんですが、.then(...).then(...).then(...) とネストしていくと、だんだん読みにくくなることがあります。
そこで登場するのが async / await です。

 asyncとは?

async function 関数名() { ... } のように async をつけた関数は、自動的にPromiseを返す関数になります。

 awaitとは?

await は「このPromiseが解決(完了)するまで、この行でいったん待って。

終わったら結果ちょうだい」という意味になります。

ポイントは、

  • awaitはasyncの中でしか使えない
  • ソースコードが「同期っぽい読みやすさ」に近づく

ということです。

 async/await

// 指定した秒数だけ待ってからメッセージを返すPromise関数
function pauseAndLog(sec) {
  return new Promise((finish) => {
    setTimeout(() => {
      const now = new Date();
      finish(
        `pauseAndLogは${sec}秒待ってから実行されました:${now.toLocaleString()}`
      );
    }, sec * 1000);
  });
}

// async関数:中でawaitが使える
async function runAsyncFlow() {
  // pauseAndLogが完了するまで待って、戻り値をmsgに入れる
  const msg = await pauseAndLog(3);
  console.log(msg);
}

// 比較用の処理
function showAlpha() {
  console.log("showAlpha: 実行開始");
}
function showBeta() {
  console.log("showBeta: 実行開始");
}

// 実行してみる
showAlpha();
runAsyncFlow(); // ← ここで3秒待つが、全体は止まらない
showBeta();

実際の流れはこうなります。

  1. showAlpha() がただちにログを出す
  2. runAsyncFlow() が呼ばれるが、そこで3秒待つPromiseを仕込んでいる
    • 3秒経ったら msg に文章が返ってきて console.log(msg) される
  3. しかしその待ち時間中も、showBeta() がすぐ実行される

await のおかげで、then(...).catch(...) のような形ではなく、”いったん待って結果を受け取る” という自然な読み方ができます。

コードがスッキリしますよね。これが実務でとても重宝します。

7. JSONってなに?なぜみんなJSONでもらいたがるの?

非同期処理が本当に活きるのは「サーバーとデータをやり取りするとき」です。
ここでしょっちゅう登場するのが JSON(JavaScript Object Notation)

JSONはざっくり言うと「JavaScriptのオブジェクトっぽい見た目をしたデータの表現ルール」です。

例:

{
  "city": "Fukuoka",
  "forecast": ["晴れ", "くもり", "雨"],
  "publishedAt": "2025-10-29T09:00:00+09:00"
}

ポイントは、

  • { ... } はオブジェクト
  • [] は配列
  • "キー": 値 のペアでデータが並んでいる
  • 文字列には基本ダブルクォーテーションを使う

JSONは軽くて読みやすいので、APIのレスポンス形式としていま標準的に使われています。
天気予報、ニュース、在庫情報、チャットのログ、フォームの送信結果など、なんでもJSONでもらうことが多いです。

8. Fetch APIでデータを取ってくる(非同期でサーバーにアクセス)

ではいよいよ実戦。
フロントエンド開発でよく使うのが Fetch API です。

Fetch API はブラウザに標準で入っている機能で、URLに対してHTTPリクエストを送り、返ってきたレスポンスをPromiseとして受け取ることができます。

  • fetch(どこに取りに行くか)
  • 返ってきたレスポンスを .json() でJSONとして読み込む
  • その結果(オブジェクト)を好きに扱う

という流れです。

 Fetch APIの基本的な書き方(Promiseスタイル)

// 天気データを配信しているAPIのURL(例)
const endpoint =
  "https://www.jma.go.jp/bosai/forecast/data/forecast/400000.json";

fetch(endpoint)
  .then((res) => {
    // 受け取ったレスポンスをJSONとして解釈
    return res.json();
  })
  .then((weatherData) => {
    console.log("受け取った生データ:", weatherData);

    // 特定エリアのデータを取り出す(例として最初のエリア)
    const targetArea = weatherData[0].timeSeries[0].areas[0];

    console.log("エリア情報:", targetArea);
    console.log("エリア名:", targetArea.area.name);
    console.log("今日の天気:", targetArea.weathers[0]);
    console.log("明日の天気:", targetArea.weathers[1]);
    console.log("あさっての天気:", targetArea.weathers[2]);

    console.log("発表元:", weatherData[0].publishingOffice);
    console.log("報告時刻:", weatherData[0].reportDatetime);
  })
  .catch((err) => {
    console.error("天気データの取得に失敗しました。", err);
  });

ここでやっていることは、

  1. fetch(...) でサーバーに問い合わせ(非同期)
  2. サーバーから返ってきたレスポンスを .json() でオブジェクト化(これも非同期で、Promiseを返す)
  3. .then(...) の中で実際のデータを扱う
  4. .catch(...) でエラーハンドリング

です。

この取得したデータは、ただconsole.logするだけじゃなく、DOMを書き換えて画面に表示することもできます。
たとえば <div id="weatherBox"></div> にテキストを差し込む、といったことができます。

9. Fetch API × async/await版(より読みやすい書き方)

同じことは async/await でも書けます。

これはとても実戦的です。

async function loadWeather() {
  const apiUrl =
    "https://www.jma.go.jp/bosai/forecast/data/forecast/400000.json";

  try {
    // サーバーにアクセス(ここで結果が返るまで待つ)
    const response = await fetch(apiUrl);

    // レスポンスをJSONとして展開(これも待つ)
    const info = await response.json();

    console.log("取得した天気データ:", info);

    // 特定エリアを取り出す(例として先頭エリア)
    const regionData = info[0].timeSeries[0].areas[0];

    console.log("地域名:", regionData.area.name);
    console.log("きょう:", regionData.weathers[0]);
    console.log("あした:", regionData.weathers[1]);
    console.log("あさって:", regionData.weathers[2]);

    // DOMに表示する例(画面に書き出す)
    const box = document.getElementById("weatherBox");
    if (box) {
      box.textContent = `${regionData.area.name} の天気: ${regionData.weathers[0]}`;
    }
  } catch (e) {
    console.error("天気情報の取得に失敗しました", e);
  }
}

// 使い方
loadWeather();

この書き方だと、

  • await fetch(...):サーバーから返ってくるのを待つ
  • await response.json():JSONパースが終わるのを待つ
  • その後は普通の変数として扱える

という流れで、同期処理っぽい読み心地になります。
「何がいつ完了するのか」「どこでエラーになるのか」が追いやすいので、メンテもしやすいです。

10. まとめ:なぜ非同期処理は避けて通れないのか

ここまでの話をまとめます。

  • 同期処理
    • 1つずつ順番に実行
    • 前の処理が終わるまで次に進まない
    • シンプルだけど、待ち時間が長いと画面が止まってしまう
  • 非同期処理
    • 「あとでやって」と予約して、すぐ次の処理に進める
    • ユーザーにとって待ち時間のストレスが減る
    • UIが止まらない・使いやすくなる
  • Promise
    • 非同期処理の「成功」「失敗」「結果の値」を表現する仕組み
    • .then(...) / .catch(...) で後続処理が書ける
  • async / await
    • Promiseをもっと読みやすく書くための文法
    • await で「Promiseが終わるまで一時停止して、結果を受け取る」が自然に書ける
  • Fetch API
    • JavaScriptからサーバーにアクセスしてデータを持ってくる標準的な方法
    • 結果はJSONで受け取ることが多い
    • 画面遷移なしで情報を更新できるから、UXが大きく向上する

あなたがこれを身につけると、「フォームを送信したときに、エラーをその場で出す」「天気予報やニュースを再読み込みなしで表示する」「APIから最新データを引っ張ってきてDOMを書き換える」といった、”いまのWebっぽい”ふるまいが自分で書けるようになります。

もう「静的なページのコーダー」ではなく、「インタラクションまで作れるフロントエンド」に一歩進んだ状態となります。

関連記事