[JavaScript講座] async / await

当ページのリンクには広告が含まれています。
目次

async 関数とは何か

async を付けると「戻り値が Promise になる」

function normal() {
  return 1;
}

async function asyncFn() {
  return 1;
}

console.log(normal());   // 1
console.log(asyncFn());  // Promise { 1 } (fulfilled なPromise)

ポイント:

  • async を付けた関数は、どんな値を返しても必ず Promise に“包まれる”
  • つまり: async function f() { return 1; } // 実質的には: function f2() { return Promise.resolve(1); } と同じイメージです。

async 関数内の throw は「reject になる」

async function errorFn() {
  throw new Error("失敗");
}

const p = errorFn();
console.log(p); // Promise { <rejected> Error: 失敗 }

errorFn()then/catch で扱うこともできます:

errorFn()
  .then(() => {
    console.log("ここには来ない");
  })
  .catch(e => {
    console.log("catch:", e.message); // "失敗"
  });
  • async 関数内で throw したエラーは、その関数が返す Promise の reject として扱われる
  • つまり「戻り値もエラーも、全部 Promise によって表現される」

await の基本挙動

await は Promise の「結果」を取り出す

await は、Promise の結果(fulfill 値 or reject エラー)を待つ構文です。

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

async function main() {
  console.log("before");
  await delay(1000);       // ここで「1秒待つ」
  console.log("after");    // 1秒後に実行
}

main();

厳密には:

  • await promise は、
    • promise が fulfilled になる → その値が await の結果
    • promise が rejected になる → そこでエラー(例外)が投げられる(=throw 相当)

await は「Promise じゃない値」にも使える

async function f() {
  const x = await 1;
  console.log(x); // 1
}

これは内部的に:

await Promise.resolve(1);

と同じ扱いです。

  • await は「引数が Promise ならその完了を待つ」「Promise じゃないなら即座に値を返す」
  • そのため、「どちらが来てもOK」なコードを書きたいときに便利

awaitasync 関数内でしか使えない

歴史的に:

  • awaitasync function の中だけ で使える構文
  • 関数の外(トップレベル)で使うと構文エラー
// NG(非モジュール環境など)
const res = await fetch("/api"); // SyntaxError

ただし:

  • ES Modules では、一部の環境で トップレベル await が使えますが、
    基本は「async 関数の中で使う」と覚えておけばOKです。

async / await で Promise チェーンを書き換える

Promiseチェーンの例

doAsync1()
  .then(result1 => doAsync2(result1))
  .then(result2 => doAsync3(result2))
  .then(result3 => {
    console.log("最終結果:", result3);
  })
  .catch(err => {
    console.error("エラー:", err);
  });

これを async/await に書き換えると:

async function main() {
  try {
    const result1 = await doAsync1();
    const result2 = await doAsync2(result1);
    const result3 = await doAsync3(result2);
    console.log("最終結果:", result3);
  } catch (err) {
    console.error("エラー:", err);
  }
}

main();
  • 手続き的な順番で書けるので、直感的に読みやすい
  • エラー処理も try/catch にまとまる

try / catch と async / await

await で reject されたら throw 相当になる

async function main() {
  try {
    const res = await fetch("/api/data"); // ここでネットワークエラーなど
    const json = await res.json();
    console.log(json);
  } catch (err) {
    console.error("非同期エラーをキャッチ:", err);
  }
}

ここで起きていること:

  1. fetch が Promise を返す
  2. それが reject された場合、await fetch(...) の行で 例外が投げられたかのように扱われる
  3. try ブロック内なので、catch に飛ぶ

関数ごとにエラーハンドリングの「境界」を作る

async 関数は、その中で起きたエラーを 上位の try/catch に伝播 させることができます。

async function fetchUser(id) {
  const res = await fetch(`/users/${id}`);
  if (!res.ok) {
    throw new Error("ユーザー取得失敗: " + res.status);
  }
  return await res.json();
}

async function main() {
  try {
    const user = await fetchUser(1);
    console.log(user);
  } catch (e) {
    console.error("アプリ全体としてのエラー処理:", e);
  }
}
  • fetchUser 内では「ユーザー取得」という責務にフォーカスして throw する
  • main では「アプリとしてどうするか」を判断する(リトライ・エラーメッセージ表示など)

こうやって、「関数ごと」に エラーの境界 を設計できるのが大きな利点です。

直列処理と並列処理(Promise.all との使い分け)

直列でやる(1つずつ順番に await

async function fetchSequential() {
  const a = await fetch("/api/a");
  const b = await fetch("/api/b");
  const c = await fetch("/api/c");

  console.log(a.status, b.status, c.status);
}
  • fetch("/api/a") が終わってから "/api/b" を呼ぶ → 完全に直列
  • 各処理が1秒かかるなら、合計約3秒

並列でやる(同時に開始してからまとめて待つ)

async function fetchParallel() {
  const pA = fetch("/api/a");
  const pB = fetch("/api/b");
  const pC = fetch("/api/c");

  const [a, b, c] = await Promise.all([pA, pB, pC]);

  console.log(a.status, b.status, c.status);
}
  • fetch を先に全部呼んでおいてから Promise.all でまとめて待つ
  • 3つそれぞれ1秒なら、全体としては「約1秒」で終わるイメージ

配列に対して並列で処理する

async function fetchMany(urls) {
  const promises = urls.map(url => fetch(url));
  const responses = await Promise.all(promises);

  return responses;
}
  • url に対して fetch をすぐに呼ぶ → すべて並列で進行
  • Promise.all で「全部成功するまで待つ」

※ エラー時の挙動(どれか1つでも reject したら Promise.all 全体が reject する)

async / await のよくある落とし穴

forEachasync を組み合わせる罠

// 期待したようには動かない例
async function process(urls) {
  urls.forEach(async (url) => {
    const res = await fetch(url);
    console.log(url, res.status);
  });
  console.log("end"); // たぶん各fetchより先に出る
}
  • forEach 自体は Promise を待たない
  • async コールバックの終了を待たないので、console.log("end") が先に実行される

正しいパターン1:for…of で直列処理

async function processSequential(urls) {
  for (const url of urls) {
    const res = await fetch(url);
    console.log(url, res.status);
  }
  console.log("end"); // 全部終わってから出る
}

パターン2:Promise.all で並列処理

async function processParallel(urls) {
  const promises = urls.map(async (url) => {
    const res = await fetch(url);
    console.log(url, res.status);
  });

  await Promise.all(promises);
  console.log("end"); // 全てのfetchが終わってから出る
}

async だけ付けて await を書き忘れる

async function main() {
  fetch("/api/data");  // await していない
  console.log("end");
}
  • これだと fetch の完了を待たずに end が出る
  • その挙動が「意図どおり」ならOKですが、
    「待ってから次に進みたい」場合は await を忘れないように注意

async 関数からの戻り値をそのまま値として扱ってしまう

async function calc() {
  return 1 + 2;
}

const x = calc();
console.log(x); // Promise {...}
  • calc() の戻り値をそのまま数値だと思うとバグる
  • 必要なら await calc().then で値を取り出す:
async function main() {
  const x = await calc();
  console.log(x); // 3
}

<<前へ(コールバックとPromise)

>>次へ(DOM操作の基礎)

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次