TypeScriptのasync/awaitとtry-catchを解説!非同期処理のエラーハンドリング
生徒
「TypeScriptで非同期処理を勉強しているのですが、もし途中でエラーが起きたらどうすればいいんですか?」
先生
「TypeScriptでは、async関数の中でtry-catchという仕組みを使うことで、エラーを安全に受け止めることができます。」
生徒
「エラーが起きたときにプログラムが止まってしまうのを防げるということでしょうか?」
先生
「その通りです!それでは、具体的な書き方とコツを詳しく解説していきますね。」
1. 非同期処理とエラーハンドリングの基本
プログラミングの世界では、インターネットからデータを取得したり、大きなファイルを読み込んだりする際に時間がかかることがあります。こうした「待ち時間」が発生する処理を非同期処理と呼びます。TypeScriptやJavaScriptでは、この非同期処理を扱うためにPromise(プロミス)やasync/await(エイシンク・アウェイト)という仕組みを使います。
しかし、インターネットの接続が切れていたり、指定したファイルが見つからなかったりすると、処理は失敗してしまいます。この「失敗」をプログラミング用語で例外やエラーと呼びます。エラーが発生したときに、何も対策をしていないと、アプリケーションは突然動かなくなってしまいます。これを防ぐために行うのがエラーハンドリングです。エラーを適切に「捕まえて」、ユーザーに「通信に失敗しました」とメッセージを出すなどの対応を行うことが、使いやすいアプリを作る鍵となります。
2. async/awaitとは何かを復習しよう
まず、エラーハンドリングの主役となるasync/awaitについて簡単におさらいしましょう。asyncは関数の前につける魔法の言葉で、「この関数は非同期処理を含みますよ」という宣言です。そして、その関数の中で使われるawaitは、「この処理が終わるまで次の行には進まずに待ってください」という命令になります。
これらを使うことで、複雑になりがちな非同期処理を、上から下へ順番に実行される普通のプログラムのように書くことができます。初心者の方にとって、この読みやすさは非常に大きなメリットです。ただし、awaitで待っている間にエラーが起きると、そこで処理が投げ出されてしまうため、次に説明する仕組みが必要になります。
3. try-catch構文の書き方と仕組み
非同期処理でエラーを捕まえるために最も一般的な方法が、try-catch(トライ・キャッチ)構文です。漢字でイメージするなら、「試しにやってみて(try)、ダメだったら捕まえる(catch)」という流れです。基本的な形は以下のようになります。
async function checkData() {
try {
// ここに実行したい処理を書きます
console.log("データの読み込みを開始します...");
} catch (error) {
// tryの中でエラーが起きたときだけ、ここが実行されます
console.log("エラーが発生しました。内容を確認してください。");
}
}
tryのブロック(波括弧で囲まれた部分)の中に、失敗する可能性があるコードを書きます。もしエラーが起きなければ、catchの中身は無視されて処理が進みます。もしエラーが起きた瞬間に、tryの中の残りの処理は中止され、すぐにcatchへとジャンプします。これにより、プログラムが強制終了するのを防ぐことができるのです。
4. awaitとtry-catchを組み合わせる実践例
それでは、実際に時間がかかる処理(疑似的な通信処理)を想定して、エラーハンドリングを書いてみましょう。以下の例では、成功する場合とエラーを発生させる場合をシミュレーションしています。
// 3秒待ってからデータを返す、またはエラーを出す関数
function fetchUserName(id: number): Promise<string> {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id === 1) {
resolve("たろう");
} else {
reject(new Error("ユーザーが見つかりません"));
}
}, 3000);
});
}
async function displayUser() {
try {
console.log("通信中...");
const name = await fetchUserName(2); // ここでエラーが発生する設定です
console.log("お名前は " + name + " です");
} catch (e) {
console.log("【エラー】ユーザー情報の取得に失敗しました。");
if (e instanceof Error) {
console.log("詳細な原因: " + e.message);
}
}
}
displayUser();
通信中...
(3秒後)
【エラー】ユーザー情報の取得に失敗しました。
詳細な原因: ユーザーが見つかりません
このコードでは、idが1以外の場合にエラーが投げられるようになっています。await fetchUserName(2)を実行したとき、エラーが発生するため、その下の「お名前は~」というログは表示されず、即座にcatchへと移動します。このように、エラーの内容を画面に表示したり、記録したりすることが可能になります。
5. catchで受け取るerrorの型と扱い方
TypeScriptでは、catch(error)のerrorの部分の型は、標準ではunknown(アンノウン)という「何が来るかわからない」という型になっています。これは、エラーとして投げられるものが文字だったり数字だったり、あるいは特別なオブジェクトだったりする可能性があるからです。
安全にエラーメッセージを取り出すためには、先ほどの例でも使ったinstanceof Errorという書き方を使います。これは、「このエラーは一般的なエラー形式のものですか?」と確認する作業です。これを行うことで、TypeScriptは「あ、これはエラーオブジェクトだから、メッセージという項目を持っているな」と理解してくれます。パソコンに詳しくない方でも、「届いた荷物の中身を確認してから開ける」という手順をイメージすれば分かりやすいでしょう。
6. 最後に必ず実行するfinallyブロック
エラーが発生しても、発生しなくても、最後には必ずやっておきたい処理がある場合があります。例えば、通信を開始したときに表示した「読み込み中アイコン」を消す作業や、開いたファイルを閉じる作業などです。そんな時に便利なのがfinally(ファイナリー)ブロックです。
async function processOrder() {
let isLoading = true;
console.log("注文処理を開始します...");
try {
// 何らかの注文処理
throw new Error("在庫切れ"); // 意図的にエラーを起こしてみます
} catch (error) {
console.log("注文に失敗しました。");
} finally {
// 成功しても失敗しても、最後に必ずここを通ります
isLoading = false;
console.log("処理を終了しました。ボタンを再活性化します。");
}
}
processOrder();
注文処理を開始します...
注文に失敗しました。
処理を終了しました。ボタンを再活性化します。
このようにfinallyを使うことで、コードの重複を防ぎ、後片付けの忘れをなくすことができます。これは、プログラミングにおいて非常に重要な「安全策」の一つです。
7. 複数の非同期処理でのエラーハンドリング
実際の開発では、複数の通信を順番に行うことがあります。例えば、「ログインして、その後にプロフィールを取得する」といった流れです。この場合、どこでエラーが起きても一つのcatchでまとめて処理することもできますし、処理ごとに個別にエラー対策をすることもできます。
まとめて処理する場合は、一つの大きなtryの中にすべてのawaitを書きます。こうすると、どこか一箇所でも失敗すればすぐにエラー処理へ移ります。一方で、特定の処理が失敗しても他の処理は続けたい、という場合は、try-catchを小分けにして書くのがコツです。自分の作りたいアプリが、エラーのときにどう動いてほしいかを考えて使い分けましょう。
8. エラーメッセージを自作する方法
ただ「エラーです」と言うだけでなく、何が原因なのかを自分たちで定義して伝えることもできます。これを「エラーを投げる」という意味でthrow(スロー)と言います。自分で条件を決めてエラーを発生させることで、より細かいエラーハンドリングが可能になります。
async function validateAge(age: number) {
try {
if (age < 0) {
// 自分の好きなメッセージでエラーを発生させる
throw new Error("年齢にマイナスの値は使えません");
}
console.log("年齢チェック完了: " + age);
} catch (error) {
if (error instanceof Error) {
console.log("バリデーションエラー: " + error.message);
}
}
}
validateAge(-5);
バリデーションエラー: 年齢にマイナスの値は使えません
このように、システムが自動的に出すエラーだけでなく、自分たちのルールに反した場合にもエラーとして扱うことで、バグの少ないしっかりしたプログラムを作ることができるようになります。初心者の方は、まずはこのtry、catch、throwの三点セットを覚えるところから始めてみましょう。
9. 初心者が気をつけるべき注意点
エラーハンドリングを学ぶ上で、陥りやすい罠がいくつかあります。一つは、catchの中で何もしないことです。エラーを捕まえたのに、何も表示せず無視してしまうと、不具合が起きたときに原因が全く分からなくなってしまいます。最低でもconsole.logで内容を表示するようにしましょう。
もう一つは、非同期関数を呼び出すときにawaitを付け忘れることです。awaitがないと、処理の完了を待たずに次の行へ進んでしまうため、try-catchの網をすり抜けてエラーが飛んでいってしまうことがあります。「非同期処理を扱うときは、必ずペアで使う」という意識を持つことが、安定したプログラミングへの第一歩です。