JavaScript
frontend

Fetch APIでネットワークエラーも含めてエラーハンドリングする

Fetch API をラップした fetchWithErrorHandling を作る

fetch APIを使うにあたって、ネットワークエラーなどのハンドリングを考えると少しエラーハンドリングの順序を工夫しないといけない。結論から言うと、以下のようなコードにすることで例えばChromeの ERR_CONNECTION_REFUSED も含めて拾うことができる。

const handleErrors = (res) => {
  if (res.ok) {
    return res;
  }

  switch (res.status) {
    case 400: throw Error('INVALID_TOKEN');
    case 401: throw Error('UNAUTHORIZED');
    case 500: throw Error('INTERNAL_SERVER_ERROR');
    case 502: throw Error('BAD_GATEWAY');
    case 404: throw Error('NOT_FOUND');
    default:  throw Error('UNHANDLED_ERROR');
  } 
};

const fetchWithErrorHandling = (url, options) =>
  fetch(url, options)
    // 1. ネットワーク周りなどのリクエスト以前の段階でのエラーを処理する
    .catch((e) => { throw Error(e); })
    // 2. サーバサイドで発行されたエラーステータスを処理する
    .then(handleErrors)
    // 3. 以上2つをパスした正常なレスポンスからJSONオブジェクトをパースする
    .then(res => res.json());

重要なポイントなのは handleErrorsAPI側のエラーを拾う前Promise.prototype.catch でネットワークエラーなどの、APIへのリクエスト自体の失敗を拾っておくこと。これが必ず最初にないと、そもそもレスポンスが返されず handleErrors の内部で res を見る処理そのものがエラーになってしまう。

fetchWithErrorHandling を使ってみる

以下は async/await と一緒に fetchWithErrorHandling を使う例。

const unauthorizedRequest = async () => {
    try {
      await fetchWithErrorHandling('https://httpstat.us/401');
    } catch (e) {
      console.log(e); // Error: UNAUTHORIZED
    }
};

unauthorizedRequest();

余談: エラーステータスを例外としてcatchすべきか否か

そもそも論として、APIからのレスポンスとしての404や401を try...catch の中でエラーとしてハンドリングするべきなのか? という点には、議論の余地がある。なぜなら、40x系や50x系のエラーレスポンスをサーバサイドから受け取れているという点で Fetch API は処理を完了しており、アプリケーションとしては正常であるから。

fetchWithErrorHandling は実装上の利便性のためにフロントエンド側のエラーとサーバサイド側のエラーをひとつにまとめて例外として処理しているが、厳密に例外処理を区分する場合には今回の実装は必ずしも適切ではない。