例外処理:try-catch文、finally、throw、およびHTTPリクエスト:fetch、axiosのわかりやすい使い方
はじめに
例外処理では、特定の例外をキャッチして処理しようとする場合、関係のない例外もキャッチしてしまう可能性があることを理解しておいてください。合わせて例外処理が発生している場合としてHTTPリクエストについても記述しています。
また、ライブラリやフレームワークで独自の例外処理が用意されている場合も多いので、その場合はそちらを使用することをお勧めします!
try { ... }
ブラック内には、同期的で例外が発生する可能性のあるコードを書きます。
この部分のコードが実行され、例外が発生した場合にその例外はcatch
ブロックに渡されます。
catch (error) { ... }
try
ブロック内で発生した例外をキャッチし、処理します。
例外が発生した場合、コードの実行がtry
ブロックからcatch
ブロックに移動します。
error
は、キャッチされた例外を表す変数で、Error
オブジェクトです。
次の使用例で作成されているError
オブジェクトは、以下のプロパティを持っています。
name: "Error"
message: "tryブロック: エラーが発生しました。"
throw
例外が発生した場合に、現在の関数の実行を中止させ、その後try
ブロック内で記述している場合にはcatch
ブロックに処理が移ります。エラーなどを示すオブジェクトを明示的に作成することができるので、エラー箇所などがわかりやすいです。
catch
ブロックで再スローしている場合については大事な内容なので、この記事のreturnとthrowの違い**のところで記述しています。
throw
文にはさまざまな型のオブジェクトを投げることができますが、一般的にはError
オブジェクトを使用すると思います。
使用例ではわかりやすいように記述していますが、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
のようにエラーの発生を通知するためには適切ではありません。
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
は解決または拒否されます。
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を扱う非同期的なコードで使用され、問題なく非同期操作が完了された時の処理を設定します。
非同期関数(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リクエストを送信するための方法
axios
とfetch
が使用されると思います。
axios
ライブラリをインストールすることで使用できます。
レスポンスデータは自動的にJSON形式に変換されます。
const response = await axios.get('/api/user');
axios.isAxiosError
与えられたエラーオブジェクトがエラーがAxios由来かどうかを判定することができます。
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
リクエストとレスポンスを受け取った後に共通の設定(リクエスト:認証トークンの追加、リクエストヘッダーの設定など、レスポンス:エラーハンドリング、データの整形など))を行うことができます。これにより、コードの重複が減ります。
// 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レスポンスを生成するために使用されます。
第一引数にはレスポンスのボディ(データなど)、第二引数にはステータスなどのレスポンスの設定を指定するオプションオブジェクトを渡します。
外部とのデータの受け渡しや保存、送信などの場面では、画像やテキストなど以外のデータは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形式の文字列に変換することができます。
const person = {
name: "John Doe",
age: 30,
};
console.log(JSON.stringify(person));
// {"name":"John Doe","age":30}
JSON文字列(JSON形式の文字列)
JSON文字列では、プロパティ名と文字列の値はダブルクォート("
)で囲む必要があります。
終わりに
何かありましたらお気軽にコメント等いただけると助かります。
ここまでお読みいただきありがとうございます🎉
Discussion