🔖

例外処理:try-catch文、finally、throw、およびHTTPリクエスト:fetch、axiosのわかりやすい使い方

2024/05/23に公開

はじめに

例外処理では、特定の例外をキャッチして処理しようとする場合、関係のない例外もキャッチしてしまう可能性があることを理解しておいてください。合わせて例外処理が発生している場合としてHTTPリクエストについても記述しています。

また、ライブラリやフレームワークで独自の例外処理が用意されている場合も多いので、その場合はそちらを使用することをお勧めします!

try { ... }

ブラック内には、同期的で例外が発生する可能性のあるコードを書きます。
この部分のコードが実行され、例外が発生した場合にその例外はcatchブロックに渡されます。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/try...catch

catch (error) { ... }

tryブロック内で発生した例外をキャッチし、処理します。
例外が発生した場合、コードの実行がtryブロックからcatchブロックに移動します。

errorは、キャッチされた例外を表す変数で、Errorオブジェクトです。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Error

次の使用例で作成されているErrorオブジェクトは、以下のプロパティを持っています。

name: "Error"
message: "tryブロック: エラーが発生しました。"

throw

例外が発生した場合に、現在の関数の実行を中止させ、その後tryブロック内で記述している場合にはcatchブロックに処理が移ります。エラーなどを示すオブジェクトを明示的に作成することができるので、エラー箇所などがわかりやすいです。

catchブロックで再スローしている場合については大事な内容なので、この記事のreturnとthrowの違い**のところで記述しています。

throw文にはさまざまな型のオブジェクトを投げることができますが、一般的にはErrorオブジェクトを使用すると思います。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/throw

使用例ではわかりやすいように記述していますが、throwは必要な場合のみ使用してむやみに例外を発生させないようにしてください。

finally { ... }

例外の発生有無に関係なく、最後に実行されます。
データベース接続を閉じるなどのリソースのクリーンアップや後処理などのために使用されます。

使用例

const checkValue = (value?: string) => {
  try {
    console.log("tryブロック");

    if (value === 'Hello, world!') {
      return "return: tryブロック";
    } else if (value && value !== 'Hello, world!') {
      throw new Error("値が正しくありません。");
    } 
  } catch (error) {
    console.log("catchブロック: " + error);
    throw error; // エラーを再スロー
  } finally {
    console.log("finallyブロック");
    return "return: finallyブロック"; // これが最終的な戻り値となります
  }
}

console.log("例外なし:", checkValue('Hello, world!')); // ①
console.log("例外あり:", checkValue('エラー!')); // ②
console.log("引数がない:", checkValue()); // ③

出力結果

①例外なし

tryブロック
finallyブロック
例外なし: return: finallyブロック

②例外あり

tryブロック
catchブロック: Error: 値が正しくありません。
finallyブロック
例外あり: return: finallyブロック

console.log内で文字列結合が行われる際、JavaScriptは自動的にerror.toString()を呼び出します。ErrorオブジェクトのtoStringメソッドは"Error: message"形式の文字列を返します。

文字列結合をしない場合には、console.log(error.message);tryブロック: エラーが発生しました。と返ってきます。

console.log(error)messageを指定しない場合には、下記のようなオブジェクトが表示されると思います。

Error: 例外が発生しました
    at <anonymous>:2:9
    at Object.InjectedScript._evaluateOn (<anonymous>:905:140)
    at Object.InjectedScript._evaluateAndWrap (<anonymous>:838:34)
    at Object.InjectedScript.evaluate (<anonymous>:694:21)

③引数がない

tryブロック
finallyブロック
引数がない:, return: finallyブロック

finallyブロックではreturnを使用しない

出力結果から、最後に必ず例外: return: finallyブロックが表示されているのがわかると思います。

この理由は、finallyブロックはtryブロックやcatchブロックが終了した後に必ず実行されるため、finallyブロック内のreturnが関数の最終的な戻り値を上書きして最終的な戻り値となってしまうからです。

そのため、tryブロックやcatchブロックでのreturnの戻り値やthrowが取得できなくなるため、finallyブロックではreturnを使用しないようにしてください。

finallyブロックのreturnの記述を削除した修正後の使用例の場合の出力結果を見てみます。

const checkValue = (value?: string) => {
  try {
    console.log("tryブロック");

    if (value === 'Hello, world!') {
      return "return: tryブロック";
    } else if (value && value !== 'Hello, world!') {
      throw new Error("値が正しくありません。");
    } 
  } catch (error) {
    console.log("catchブロック: " + error);
    throw error; // エラーを再スロー
  } finally {
    console.log("finallyブロック");
  }
}

console.log("例外なし:", checkValue('Hello, world!')); // ①
console.log("例外あり:", checkValue('エラー!')); // ②
console.log("引数がない:", checkValue()); // ③

①例外なし

修正後で変化がないため省略します。

②例外あり

tryブロック
catchブロック: Error: 値が正しくありません。
Uncaught Error: 値が正しくありません。

③引数がない

tryブロック内でreturn文が実行されず、catchブロックも実行されないため以下のような結果になります。

tryブロック
finallyブロック
引数がない", undefined

returnとthrowの違い

return

throw error; // エラーを再スローの箇所をreturn error;としても良いのですが、returnは関数から値を返すために使用されます。

関数が正常に終了し、戻り値を返す場合に使用されるので、throwのようにエラーの発生を通知するためには適切ではありません。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/return

throw

returnとの大きな違いにエラーの再スローがあります。

エラーを再スローすることで、現在のスコープ内で適切に処理できなかったエラーを関数の呼び出し元に伝達し、エラーハンドリングを継続させることができます。

const checkValue = (value?: string) => {
  try {
    console.log("tryブロック");

    if (value === 'Hello, world!') {
      return "return: tryブロック";
    } else if (value && value !== 'Hello, world!') {
      throw new Error("値が正しくありません。");
    }
  } catch (error) {
    console.log("catchブロック: " + error);
    throw error; // エラーを再スロー
  } finally {
    console.log("finallyブロック");
  }
}

try {
  console.log("例外あり:", checkValue('エラー!')); // ②
} catch (e) {
  console.log("関数呼び出し元のcatchブロックでキャッチ:", e.message);
}

関数呼び出し元のcatchブロックでエラーをキャッチできていることがわかると思います。
出力結果

tryブロック
catchブロック: Error: 値が正しくありません。
finallyブロック
関数呼び出し元のcatchブロックでキャッチ: 値が正しくありません。

最初の使用例のように呼び出し元の関数にcatchブロックが存在しない場合は、プログラムが終了します。

const checkValue = (value?: string) => {
  try {
    console.log("tryブロック");

    if (value === 'Hello, world!') {
      return "return: tryブロック";
    } else if (value && value !== 'Hello, world!') {
      throw new Error("値が正しくありません。");
    }
  } catch (error) {
    console.log("catchブロック: " + error);
    throw error; // エラーを再スロー
  } finally {
    console.log("finallyブロック");
  }
}

Promise

非同期操作の完了や失敗を表すJavaScriptのオブジェクトです。

Promiseは、3つの状態を持ちます:未解決(pending)、解決済み(fulfilled)、拒否(rejected)。非同期操作が完了すると、Promiseは解決または拒否されます。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise

APIにリクエストを送信する処理は非同期なので下記では、レスポンスが返ってくるのを待ってから次の処理を実行しています。

const fetchData = () => {
  return new Promise((resolve, reject) => {
    // 非同期処理
    fetch('/api/user')
      .then(response => {
        if (!response.ok) {
          throw new Error('Error: Unable to fetch data');
        }
        return response.json();
      })
      .then(data => {
        // データの取得が成功した場合
        resolve('Success: Data fetched successfully');
      })
      .catch(error => {
        // データの取得が失敗した場合
        reject(error.message);
      });
  });
}

// 非同期処理の実行
fetchData()
  .then((data) => {
    // データの取得が成功した場合の処理
    console.log(data);
  })
  .catch((error) => {
    // データの取得が失敗した場合の処理
    console.error(error);
  });

then

主にPromiseを扱う非同期的なコードで使用され、問題なく非同期操作が完了された時の処理を設定します。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise/then

非同期関数(async-await構文)内での非同期処理の結果を待機し、エラーをハンドリングする場合は、then-catchよりもtry-catch構文を使う方が適しています。

async/await

非同期処理をPromiseよりも直感的かつ同期的に扱えるようにするための構文です。

const fetchDataAsync = async () => {
  try {
    const response = await axios.get('/api/user');
    console.log(response);
  } catch (error) {
    console.error(error);
  }
};

fetchDataAsync();

JavaScriptでHTTPリクエストを送信するための方法

axiosfetchが使用されると思います。

axios

ライブラリをインストールすることで使用できます。
レスポンスデータは自動的にJSON形式に変換されます。

const response = await axios.get('/api/user');

axios.isAxiosError

与えられたエラーオブジェクトがエラーがAxios由来かどうかを判定することができます。
https://github.com/axios/axios?tab=readme-ov-file#typescript

const getData = async () => {
      try {
        const response = await axios.get('/api/user');
      } catch (error) {
        // エラーがAxios由来かどうかを判定
        if (axios.isAxiosError(error)) {
          console.error('Axiosのエラーが発生しました:', error);
        } else {
          console.error('Axios以外のエラーが発生しました:', error);
        }
      }
    };

Interceptors

https://axios-http.com/docs/interceptors

リクエストとレスポンスを受け取った後に共通の設定(リクエスト:認証トークンの追加、リクエストヘッダーの設定など、レスポンス:エラーハンドリング、データの整形など))を行うことができます。これにより、コードの重複が減ります。

// Axiosインスタンスを作成
const apiClient = axios.create();

// リクエストインターセプターを設定
apiClient.interceptors.request.use(
  (config) => {
    // リクエストの前に共通処理を追加

    // 必要に応じてリクエストヘッダーを設定
    // config.headers['Authorization'] = 'Bearer YOUR_TOKEN';

    return config;
  },
  (error) => {
    // リクエストエラー処理
    console.error('Request Error:', error);
    return Promise.reject(error);
  }
);

// レスポンスインターセプターを設定
apiClient.interceptors.response.use(
  (response) => {
    // レスポンスの前に共通処理を追加
    console.log('Response Data:', response.data);
    return response;
  },
  (error) => {
    // レスポンスエラー処理
    console.error('Response Error:', error);
    return Promise.reject(error);
  }
);

return Promise.reject(error);

エラーをプロミスチェーンの上位に伝播させるために使われます。これがないと、エラーがその場でキャッチされてしまい、呼び出し元でエラーを処理することができなくなります。これにより、catchブロックでエラーを適切に処理することが難しくなります。

Interceptorsを使用しない場合

import axios from 'axios';

const fetchData = async () => {
  try {
    const res = await axios.get('/api/data', {
      headers: {
        'Authorization': 'Bearer YOUR_TOKEN',
      }
    });
    console.log('Data:', res.data);
  } catch (error) {
    console.error('Error:', error.response?.data);
  }
};

const fetchAnotherData = async () => {
  try {
    const res = await axios.get('/api/another-data', {
      headers: {
        'Authorization': 'Bearer YOUR_TOKEN',
      }
    });
    console.log('Another Data:', res.data);
  } catch (error) {
    console.error('Error:', error.response?.data);
  }
};

Interceptorsを使用する場合

import axios from 'axios';

// Axiosインスタンスを作成
const apiClient = axios.create();

// リクエストインターセプターを設定
apiClient.interceptors.request.use(
  (config) => {
    config.headers['Authorization'] = 'Bearer YOUR_TOKEN';
    return config;
  },
  (error) => {
      console.error('Request Error:', error);
    // エラーを返す
    return Promise.reject(error);
  }
);

// レスポンスインターセプターを設定
apiClient.interceptors.response.use(
  (response) => response,
  (error) => {
    console.error('Error:', error.response?.data);
    // エラーを返す
    return Promise.reject(error);
  }
);

const fetchData = async () => {
  try {
    const res = await apiClient.get('/api/data');
    console.log('Data:', res.data);
  } catch (error) {
    // 追加のエラーハンドリングが不要
  }
};

const fetchAnotherData = async () => {
  try {
    const res = await apiClient.get('/api/another-data');
    console.log('Another Data:', res.data);
  } catch (error) {
    // 追加のエラーハンドリングが不要
  }
};

fetch

追加のライブラリが必要なく使用できます。
レスポンスからJSONを取得するには、追加の.json()メソッドを呼び出す必要があります。

const response = await fetch('/api/user');
const data = await response.json();

Response

HTTPレスポンスを生成するために使用されます。
第一引数にはレスポンスのボディ(データなど)、第二引数にはステータスなどのレスポンスの設定を指定するオプションオブジェクトを渡します。
https://developer.mozilla.org/ja/docs/Web/API/Response/Response

外部とのデータの受け渡しや保存、送信などの場面では、画像やテキストなど以外のデータはJSON形式の文字列として扱うことが一般的なので、レスポンスのボディにデータを入れる場合にはJSON形式の文字列にしています。

resが既にJSON形式であり、そのままレスポンスボディとして使用する場合

import { client } from '../../../../libs/client';

export async function GET() {
  try {
    const res = await client.get({
      endpoint: process.env.END_POINT || '',
    });
    return new Response(res), {
      status: 200,
      headers: {
        'Content-Type': 'application/json',
      },
    });
  } catch (error) {
    throw error;
  }
}

resが既にJSON形式ではない場合、JSON形式に変換してからレスポンスボディとして使用してください。

import { client } from '../../../../libs/client';

export async function GET() {
  try {
    const res = await client.get({
      endpoint: process.env.END_POINT || '',
    });

    return new Response(JSON.stringify(res), {
      status: 200,
      headers: {
        'Content-Type': 'application/json',
      },
    });
  } catch (error) {
    throw error;
  }
}

JSON.stringify

JavaScriptのオブジェクトや値をJSON形式の文字列に変換することができます。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify

const person = {
    name: "John Doe",
    age: 30,
};

console.log(JSON.stringify(person));  
// {"name":"John Doe","age":30}

JSON文字列(JSON形式の文字列)

JSON文字列では、プロパティ名と文字列の値はダブルクォート(")で囲む必要があります。

終わりに

何かありましたらお気軽にコメント等いただけると助かります。
ここまでお読みいただきありがとうございます🎉

Discussion