TypeScriptでthrowを使った型安全な例外処理を徹底解説!エラーハンドリングの基本
生徒
「プログラムを実行しているときに、急にエラーが出て止まってしまうことがあります。TypeScriptで上手にエラーを扱う方法はありますか?」
先生
「エラーは怖いものではありませんよ。TypeScriptでは、throwという仕組みを使って、エラーが発生したことをプログラムに知らせ、安全に処理を続ける方法があります。」
生徒
「型安全にエラーを扱うというのは、どういうことでしょうか?」
先生
「それは、どんなエラーが起きる可能性があるかを事前に把握し、プログラムが壊れないように準備しておくことです。具体的な書き方を一緒に学んでいきましょう!」
1. 例外処理とthrowの基本的な考え方
プログラミングの世界では、予期しない出来事のことを例外と呼びます。例えば、インターネットがつながっていないのにデータを取得しようとしたり、数字が入るべき場所に文字が入っていたりする場合です。これらの問題が起きたとき、何も対策をしていないとプログラムはそこで真っ白になって止まってしまいます。これを防ぐのが例外処理です。
TypeScriptにおいて、自分から「ここでエラーが起きたよ!」と宣言することをthrow(スロー)と言います。日本語で投げると訳される通り、エラーというボールを投げて、それを別の場所で受け取って対処するイメージです。このようにエラーを意図的に発生させることで、プログラムの暴走を防ぎ、利用者に対して分かりやすいメッセージを表示できるようになります。
初心者の方は、まず「エラーは隠すものではなく、正しく投げて正しく受け取るもの」という感覚を身につけることが大切です。これができるようになると、バグの少ない、信頼性の高いシステムを作ることができるようになります。
2. try-catch構文でエラーを捕まえる方法
エラーを投げたら、それを捕まえる場所が必要です。そのために使うのがtry...catchという構文です。tryのブロックの中にエラーが起きそうな処理を書き、もしエラーが発生したらcatchのブロックへ移動して後始末を行います。
パソコンの操作に例えると、料理のレシピを見ているようなものです。材料がないというエラーが起きたら、コンビニに買いに行くという代替案を実行するような流れです。TypeScriptでは、この仕組みを使うことで、エラーが起きてもアプリ全体が終了してしまうのを防ぐことができます。
function checkAge(age: number) {
if (age < 0) {
// エラーを投げます
throw new Error("年齢にマイナスの値は入力できません。");
}
console.log("年齢は正常です:" + age);
}
try {
// ここで関数を実行します
checkAge(-5);
} catch (e) {
// エラーが発生した場合はここに来ます
console.log("エラーをキャッチしました!");
}
上記のコードを実行すると、次のような結果になります。プログラムが途中で強制終了せず、最後まで動いていることが分かります。
エラーをキャッチしました!
3. Errorオブジェクトとは何か
先ほどのコードに登場したnew Error()というのは、エラー専用の道具箱を作るようなものです。TypeScript(およびJavaScript)には、エラー情報を管理するための標準的な仕組みが備わっています。これを使うと、単なる文字列を投げるよりも、どこでエラーが起きたのかといった詳細な情報を保持しやすくなります。
この道具箱の中には、主にメッセージという情報が入っています。初心者のうちは、このメッセージを画面に表示させることで、自分自身や利用者が「何が原因で失敗したのか」を即座に判断できるようになります。文字列だけを投げることも可能ですが、特別な理由がない限りはnew Error()を使うのが、プログラミングの世界での一般的なマナーです。
4. TypeScriptでの型安全なキャッチの仕組み
TypeScriptの大きな特徴は、データに型をつけて、間違いを事前に見つける機能があることです。しかし、実は例外処理におけるcatchの引数は、標準ではunknownという「何が来るか分からない」という型になっています。なぜなら、エラーとして投げられるものはエラーオブジェクトだけでなく、数字や文字など、何でも投げることができてしまうからです。
そこで、型安全に処理を行うためには、受け取ったエラーが本当にErrorという形をしているかどうかを確認する作業が必要になります。これを型ガードと呼びます。これを行うことで、プログラムが「これは確かにエラーメッセージを持っているオブジェクトだ」と確信を持って処理を進められるようになります。
try {
throw new Error("通信に失敗しました");
} catch (error: unknown) {
// errorがErrorのインスタンスかどうかを確認します
if (error instanceof Error) {
// ここでは安全にmessageプロパティにアクセスできます
console.log("エラーメッセージ:" + error.message);
} else {
console.log("予期しないエラーが発生しました");
}
}
エラーメッセージ:通信に失敗しました
このように書くことで、TypeScriptは「もしエラー以外の変なものが飛んできても大丈夫」という状態を保つことができます。これが型安全な例外処理の第一歩です。
5. 独自のカスタムエラーを作って区別する
アプリケーションが大きくなってくると、一つのエラーだけでなく、色々な種類のエラーを使い分けたくなることがあります。例えば「入力ミスによるエラー」と「サーバーがダウンしているエラー」では、対処法が異なりますよね。そんなときは、自分専用のエラークラスを作ることができます。
これをカスタムエラーと呼びます。既存のErrorという仕組みを継承(コピーして拡張)することで、自分たちの都合に合わせた特別なエラーを作れるようになります。これにより、特定のエラーだけを特別扱いして処理を分けるといった、高度な制御が可能になります。
// 入力エラー専用のクラスを作ります
class InputError extends Error {
constructor(message: string) {
super(message);
this.name = "InputError";
}
}
function processInput(input: string) {
if (input === "") {
throw new InputError("文字を入力してください。");
}
}
try {
processInput("");
} catch (error) {
if (error instanceof InputError) {
console.log("入力エラーを検知:" + error.message);
}
}
入力エラーを検知:文字を入力してください。
6. Finallyブロックで必ず実行する処理を書く
例外処理には、tryとcatchの他にもう一つ、finallyという強力な味方がいます。これは、エラーが起きても起きなくても、最後に必ず実行されるという魔法のようなブロックです。よく使われる場面は、ファイルを開いた後に必ず閉じる処理や、通信を開始した後に必ず終了させる処理などです。
お片付けのようなものだと考えると分かりやすいでしょう。料理が成功しても、途中で失敗して出前を頼むことになっても、キッチンを掃除するという作業は必ず必要です。このfinallyを使うことで、プログラムのやり残しをなくし、パソコンのメモリを無駄に使い続けるようなトラブルを防ぐことができます。
function openDatabase() {
console.log("データベースを開きました");
try {
// 何らかの処理(エラーが発生する可能性があるとする)
console.log("データを書き込み中...");
throw new Error("書き込み失敗");
} catch (e) {
console.log("エラーが発生したのでログに記録します");
} finally {
// エラーの有無にかかわらず実行されます
console.log("データベースを安全に閉じました");
}
}
openDatabase();
データベースを開きました
データを書き込み中...
エラーが発生したのでログに記録します
データベースを安全に閉じました
7. 非同期処理における例外処理の注意点
最近のプログラミングでは、データの読み込み待ちなど、時間がかかる処理を裏で行う非同期処理がよく使われます。この非同期処理の中でthrowを使うときは、少し注意が必要です。通常のtry-catchだけでは、裏側で起きたエラーを捕まえられないことがあるからです。
非同期処理ではasyncやawaitというキーワードを使いますが、これらを組み合わせることで、非同期のエラーもまるで普通のプログラムのように綺麗に処理できるようになります。インターネットから画像を持ってくるような処理を作る際は、この方法が必須となります。初心者のうちは、難しいかもしれませんが「通信のときは特別な書き方があるんだな」と覚えておくだけでも十分です。
8. エラーを投げすぎないためのベストプラクティス
最後に、エラー処理のコツについてお話しします。何でもかんでもthrowすれば良いというわけではありません。プログラムが少し間違えただけで毎回エラーを投げていると、処理が複雑になりすぎてしまいます。予測できる範囲の間違い(例えばユーザーがパスワードを間違えたなど)は、エラーではなく普通の条件分岐で対応するのが良い場合もあります。
本当に深刻な問題、つまりプログラムをそのまま進めるとデータが壊れてしまうような重大な事態にこそ、throwを活用すべきです。適切な場所で適切なエラーを投げることで、自分以外の開発者や、将来の自分自身がプログラムを読み返したときに、どこで何に気をつければ良いのかが明確になります。これが、メンテナンスのしやすい、美しいコードを書くための秘訣です。