JavaScript | 非同期処理:async / await – try / catch との併用

JavaScript JavaScript
スポンサーリンク

async / await と try / catch を一言でいうと

async / await と try / catch を組み合わせると、
「非同期処理の成功もエラーも、同期コードと同じように書ける」 ようになります。

  • await で Promise の「成功結果」を普通の値として受け取り
  • その Promise が reject されたときは、その場で throw されたのと同じように扱われ
  • try / catch でそのエラーをまとめて受け止める

という形です。

ここが重要です。
「Promise の then / catch チェーン」よりも、
try / catch で一気に囲む」ほうが、
読みやすくて直感的なエラーハンドリングができる、というのが最大のメリットです。


await とエラー(reject)の関係をまず押さえる

Promise が reject されたとき、await では何が起きるか

await は、Promise が resolve(成功)したときは「中身の値」を返しますが、
Promise が reject(失敗)したときは、その場で例外が投げられます

Promise だけで書くとこうなる処理を:

someAsync()
  .then(value => {
    console.log("成功:", value);
  })
  .catch(err => {
    console.error("エラー:", err);
  });
JavaScript

async / await で書くと、
中ではこういうことが起きています。

async function run() {
  const value = await someAsync(); // ここで失敗すると throw されるイメージ
  console.log("成功:", value);
}
JavaScript

この await someAsync() のところで、
reject された場合は 同期コードの throw と同じ扱い になります。

「await は、成功時には値、失敗時には throw」

整理すると:

  • 成功(resolve) → await の戻り値として結果が返る
  • 失敗(reject) → await の行で例外が投げられる(throw と同じ)

だからこそ、後ろで try / catch が効くようになります。

ここが重要です。
「await = Promise の中身を取り出す」だけでなく、
「await = resolve は戻り値、reject は throw」
という二面性を持っている、と意識してください。
この前提が分かると、try / catch との組み合わせがストンと腑に落ちます。


try / catch と async / await の基本形

一番シンプルなパターン

まずは典型パターンから。

async function run() {
  try {
    const result = await someAsync();
    console.log("成功:", result);
  } catch (err) {
    console.error("エラー:", err);
  }
}
JavaScript

流れはこうです。

  1. await someAsync() を実行
  2. 成功 → result に値が入り、console.log へ進む
  3. 失敗 → その場で throw → catch に飛ぶ → エラーログ

この「成功の流れ」と「失敗の流れ」が、
同期コードとほぼ同じ形で表現できます。

複数の await をまとめて try / catch で囲む

async function run() {
  try {
    const a = await asyncA();
    const b = await asyncB(a);
    const c = await asyncC(b);

    console.log("全部成功:", a, b, c);
  } catch (err) {
    console.error("どこかでエラー発生:", err);
  }
}
JavaScript

ここでは、

  • asyncA で失敗しても
  • asyncB で失敗しても
  • asyncC で失敗しても

全て同じ catch ブロックに飛んできます。

ここが重要です。
Promise の .then().then().catch() でやっていたことを、
try の中に await を並べて、catch でまとめて受け止める」スタイルに変えることで、
非同期のエラーハンドリングが“普通の処理”と同じ見た目になります。


then / catch との対応関係をしっかり掴む

Promise 版と async / await 版を並べて比較

Promise チェーンで書いた場合:

someAsync()
  .then(result => {
    return nextAsync(result);
  })
  .then(finalResult => {
    console.log("成功:", finalResult);
  })
  .catch(err => {
    console.error("エラー:", err);
  });
JavaScript

同じ処理を async / await + try / catch で書くと:

async function run() {
  try {
    const result = await someAsync();
    const finalResult = await nextAsync(result);
    console.log("成功:", finalResult);
  } catch (err) {
    console.error("エラー:", err);
  }
}
JavaScript

ここで対応関係を整理すると:

  • then の中身 → 各 await の後ろの行
  • .catchcatch ブロック

という変換になります。

ここが重要です。
「Promise チェーンで書かれているコード」は、
頭の中で「await に置き換えたらどうなるか?」と翻訳してみると、
エラーフローの理解が一気に深まります。
catchtry / catch のどこに相当するのかを意識して読む練習をしてみてください。


try / catch を「どこまで」かけるか

全体をざっくり 1 つの try / catch で囲む

一番簡単なのは、「関数の中身ほぼ全部」を try / catch で囲んでしまうことです。

async function run() {
  try {
    const user = await fetchUser();
    const posts = await fetchPosts(user.id);
    const comments = await fetchComments(posts[0].id);

    console.log("結果:", user, posts, comments);
  } catch (err) {
    console.error("どこかでエラー:", err);
  }
}
JavaScript

「細かい場所にこだわらず、とにかく失敗したらここに来てほしい」というときはこの形で十分です。

部分的に try / catch を分ける

一方で、「この await のエラーだけ特別な扱いをしたい」ということもあります。

async function run() {
  let user;

  try {
    user = await fetchUser();
  } catch (err) {
    console.error("ユーザー取得に失敗:", err);
    return; // ここで終わるなど
  }

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

このように

  • ユーザー取得エラー → そこで処理終了
  • 投稿取得エラー → エラーを出しつつも、ユーザー情報は利用可能

のように、エラーの種類ごとに try / catch を分割できます。

ここが重要です。
「どこからどこまでを 1 つの try / catch にするか?」は設計のポイントです。
“全部まとめて失敗なら一緒”でいいなら 1 個で囲む。
“ここだけ特別扱いしたい”なら try / catch を分割する。
エラーに対してどう振る舞いたいかから逆算して、範囲を決めていきます。


finally との組み合わせ(後片付け処理)

try / catch / finally の基本

finally ブロックは、
成功しても失敗しても「最後に必ず実行したい処理」を書く場所 です。

async function run() {
  console.log("開始");
  try {
    const result = await someAsync();
    console.log("成功:", result);
  } catch (err) {
    console.error("エラー:", err);
  } finally {
    console.log("ここは成功/失敗に関わらず必ず実行される");
  }
}
JavaScript

例えば、

  • ローディング表示の ON/OFF
  • ファイルや接続のクローズ処理
  • 「処理中フラグ」を false に戻す

などを finally に置くケースがよくあります。

ローディングフラグの例

let isLoading = false;

async function run() {
  isLoading = true;
  try {
    const result = await someAsync();
    console.log("成功:", result);
  } catch (err) {
    console.error("エラー:", err);
  } finally {
    isLoading = false; // 成功でも失敗でも必ず false に戻す
  }
}
JavaScript

ここが重要です。
非同期処理では、「途中で失敗しても、後片付けはしたい」という場面が多くなります。
try / catch / finally の 3 つセットで、
“成功の流れ” “失敗の流れ” “どちらでもやる後始末” を設計できる、という感覚を身につけてください。


よくある勘違い・落とし穴

try / catch の外側で起きたエラーは捕まえられない

async function run() {
  try {
    const data = await someAsync();
  } catch (err) {
    console.error("エラー:", err);
  }

  // ここで await してエラーになると、この try / catch では捕まえられない
  const another = await anotherAsync();
}
JavaScript

anotherAsync()await している行は、
try ブロックの外側なので、この try / catch では捕まえられません。

解決策はシンプルで、その await を含めて try の中に入れることです。

async function run() {
  try {
    const data = await someAsync();
    const another = await anotherAsync();
  } catch (err) {
    console.error("どちらかでエラー:", err);
  }
}
JavaScript

catch の中でさらに throw した場合

async function run() {
  try {
    await someAsync();
  } catch (err) {
    console.error("一度目の catch:", err);
    throw err; // ここでもう一度投げる
  }
}
JavaScript

run() の呼び出し側で、さらに try / catch を置けば、
そこでもう一度エラーを捕まえることができます。

async function main() {
  try {
    await run();
  } catch (err) {
    console.error("呼び出し側での catch:", err);
  }
}
JavaScript

ここが重要です。
try / catch は「その関数の中で閉じた世界」ではありません。
catch の中で throw し直せば、呼び出し側にエラーをバトンタッチできる。
どこで「握りつぶす」のか、どこまで「上に伝える」のかを意識して、
catch の書き方を選ぶ必要があります。


初心者として async / await と try / catch で本当に押さえてほしいこと

await は、「Promise が resolve したら結果の値を返し、reject されたらその場で throw される」。
だから、try / catch で同期コードと同じようにエラー処理ができる。

複数の await を 1 つの try で囲めば、「どれか 1 つでも失敗したら同じ catch に飛ぶ」流れを作れる。
エラーを細かく分けて扱いたいときは、try / catch を分割して書く。

finally を使えば、成功・失敗に関わらず「必ず実行したい後処理」を書ける。
ローディング解除、フラグのリセット、リソースの解放などに便利。

try / catch の外側にある await は、その中では捕まえられない。
エラーを捕まえたい await は必ず try の中に含める。

ここが重要です。
async / await と try / catch を組み合わせると、
「非同期処理だけど、見た目も構造も同期コードそのもの」に近づけられます。
非同期の“時間的なややこしさ”を、コードの見た目からほとんど消してくれるのが、この組み合わせの本当の価値です。

最後に、理解を深めるための練習を 2 つ置いておきます。

// 練習 1:
// 1秒後に成功する Promise または失敗する Promise をランダムに返す関数 randomAsync() を作り、
// async / await + try / catch で結果をログに出してみる。

// 練習 2:
// fetch を使って API から JSON を取得する関数 fetchJson(url) を作り、
// ネットワークエラーや JSON パースエラーを try / catch で捕まえて、
// エラー内容に応じたメッセージを出し分けてみる。
JavaScript

実際に「成功したとき」と「わざと失敗させたとき」の挙動を見比べながら、
「どこで await して、どこで catch するか」を自分なりに調整してみると、
async / await と try / catch の感覚がかなり鮮明に掴めてくるはずです。

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