【JavaScript】非同期処理のエラーハンドリング(try-catch)

javascript-icon JavaScript

こんにちは、かつコーチです。

前回は「JSONの扱い方」で、APIとやり取りするデータの形式について解説しました。

ここまでの記事では、通信が「成功する」前提でコードを書いてきました。

でも実際の開発では、通信エラーやサーバー側の不具合など、うまくいかないケースへの対応が欠かせません。

今回は、非同期処理におけるエラーハンドリングの基本、try-catch を解説していきます。

なぜエラーハンドリングが必要なのか

エラーが起きる代表的なケース

fetchを使ったAPI通信では、次のような場面でエラーや想定外の結果が発生します。

  • インターネットに接続できていない
  • APIのURLを間違えている
  • サーバー側で障害が起きている
  • 存在しないデータを取得しようとした(404エラーなど)

これらへの対処を何もしていないと、エラーが起きた瞬間にプログラムが止まってしまったり、ユーザーには何も表示されないまま画面が固まったように見えてしまったりします。

エラーハンドリングをしないとどうなるか

エラー処理がないコードで何が起きるか、実際に確認してみましょう。

async function getUser() {
  const response = await fetch("https://存在しないドメイン.example/users/1");
  const data = await response.json();
  console.log(data);
}

getUser();
// Uncaught (in promise) TypeError: Failed to fetch

存在しないURLにアクセスすると、コンソールにエラーが表示され、それ以降の処理(console.log(data) など)は実行されません。

ユーザーからすると、「ボタンを押したのに何も起きない」ように見えてしまう、ちょっと不親切な状態です。

try-catchの基本

基本の書き方

try-catch を使うと、エラーが起きたときの処理を明示的に書けるようになります。

async function getUser() {
  try {
    const response = await fetch("https://jsonplaceholder.typicode.com/users/1");
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error("データの取得に失敗しました", error);
  }
}

getUser();

try ブロックの中でエラーが発生すると、その時点で処理が中断され、catch ブロックの中の処理が実行されます。

catch (error)error には、実際に発生したエラーの情報(メッセージなど)が入っています。

finallyで後片付けの処理を書く

try-catch には、成功しても失敗しても必ず実行される finally ブロックも用意できます。

async function getUser() {
  try {
    const response = await fetch("https://jsonplaceholder.typicode.com/users/1");
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error("データの取得に失敗しました", error);
  } finally {
    console.log("通信処理が終わりました"); // 成功・失敗どちらでも実行される
  }
}

getUser();

後述するローディング表示の「非表示にする」処理など、成功・失敗にかかわらず必ず行いたい後片付けは finally にまとめると読みやすくなります。

HTTPステータスコードのエラーもキャッチする

fetchの落とし穴:404や500ではエラーにならない

ここは中級者でも見落としやすいポイントなので、しっかり押さえておきましょう。

実は、fetchサーバーが「404 Not Found」や「500 Internal Server Error」を返してきても、それ自体ではエラー扱いになりません

async function getUser() {
  try {
    // 存在しないIDを指定(サーバーは404を返す)
    const response = await fetch("https://jsonplaceholder.typicode.com/users/9999");
    const data = await response.json();
    console.log("取得結果:", data); // catchに入らず、ここが実行されてしまう
  } catch (error) {
    console.error("エラー:", error);
  }
}

getUser();

fetchが catch に入るのは、「ネットワークに接続できない」など通信そのものが成立しなかった場合だけです。

サーバーがきちんと応答さえすれば、その中身が404であっても200(成功)であっても、fetch自身は成功したものとして扱われます。

私はこの仕様を知らなかったとき、「エラー処理を書いたのに、404のときだけ画面がおかしな表示になる」という現象に悩まされました。

try-catch を書いたから安心、と思い込んでいたのが原因でした。

response.okでステータスを確認する

この落とし穴に対応するには、response.ok というプロパティを使います。

❌ Before:ステータスコードを確認せずデータを使ってしまう

async function getUser(id) {
  try {
    const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
    const data = await response.json();
    document.getElementById("result").textContent = data.name; // 404でもここが動く
  } catch (error) {
    console.error("エラー:", error);
  }
}

getUser(9999);

✅ After:response.okでステータスを確認してからthrowする

async function getUser(id) {
  try {
    const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);

    if (!response.ok) {
      // ステータスコードが200番台以外なら、意図的にエラーを発生させる
      throw new Error(`ユーザーが見つかりません(status: ${response.status})`);
    }

    const data = await response.json();
    document.getElementById("result").textContent = data.name;
  } catch (error) {
    console.error("エラー:", error.message);
    document.getElementById("result").textContent = "データを取得できませんでした";
  }
}

getUser(9999);

response.ok は、ステータスコードが200〜299の範囲であれば true になるプロパティです。

if (!response.ok) で範囲外だった場合に throw new Error() を使ってあえてエラーを発生させることで、catch ブロックにきちんと処理を渡せるようになります。

「fetchはネットワークエラーしか自動でcatchしてくれない」という前提を覚えておくと、この対応を忘れずに済みます。

エラー時にユーザーへ分かりやすく伝える

エラーメッセージを画面に表示する

コンソールにエラーを出すだけでは、実際に使っているユーザーには何も伝わりません。

async function getUser(id) {
  const resultElement = document.getElementById("result");

  try {
    const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);

    if (!response.ok) {
      throw new Error(`status: ${response.status}`);
    }

    const data = await response.json();
    resultElement.textContent = data.name;
  } catch (error) {
    console.error("取得エラー:", error);
    resultElement.textContent = "データの取得に失敗しました。時間をおいて再度お試しください。";
  }
}

getUser(9999);

catch ブロックの中で、開発者向けの詳細情報は console.error に、ユーザー向けの分かりやすいメッセージは画面表示に、と役割を分けるのがポイントです。

「何が起きたか(開発者向け)」と「どうすればいいか(ユーザー向け)」を分けて考えると、エラーメッセージの設計がしやすくなります。

まとめ

この記事のポイント

  • 非同期処理では try-catch でエラーが起きたときの処理を明示的に書く
  • finally は成功・失敗にかかわらず必ず実行される後片付け処理に使う
  • fetchは404や500のステータスコードだけでは自動的にエラーにならないため、response.ok で確認する
  • エラー発生時は、コンソール向けと画面表示向けでメッセージの役割を分けると親切になる

次に読むべき記事

エラーハンドリングと合わせて実装しておきたいのが、通信中であることをユーザーに伝える「ローディング表示」です。

→ 次の記事:ローディング表示を実装する(非同期処理中のUI)

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