【TypeScript】カスタムエラーのすすめ
TypeScriptで開発をしていると、APIエラーやバリデーションエラーなど、さまざまなエラーを扱う場面があります。
そんなときに、標準のErrorクラスだけで対応していませんか。
この記事では、カスタムエラーを導入するメリットと、ボイラープレートを減らしてカスタムエラーを楽に定義出来るライブラリを紹介します。
カスタムエラーを作る理由
標準のErrorクラスを使用することで楽にエラーを作成できますが、次のような問題があります。
- エラーの種類を区別しづらい
- 追加の情報(HTTPステータスやエラーコードなど)を持たせづらい
- メッセージが一貫しない
たとえば次のような例を考えてみましょう。
try {
throw new Error('User not found');
} catch (error) {
if (error.message.includes('not found')) {
// ...
}
}
これでは、エラーの種類を文字列で判定することになり、変更に弱く、可読性も低くなります。
エラーを明確に区別したい場合に、カスタムエラーが役に立ちます。
カスタムエラーの作り方
まずはライブラリを使わずに、シンプルなカスタムエラーを作る方法を見てみましょう。
class NotFoundError extends Error {
constructor(message: string) {
super(message);
this.name = 'NotFoundError';
}
}
try {
throw new NotFoundError('User not found');
} catch (error) {
if (error instanceof NotFoundError) {
console.error(error.toString());
// NotFoundError: User not found
}
}
一見、シンプルに見えますが、実際には、this.name
を毎回設定しなければならないという手間があります。
JavaScriptのErrorクラスは継承時にname
が自動的にクラス名に設定されないため、これを明示的に指定しないと、すべてのカスタムエラーのname
が単にErrorになってしまいます。
Errorクラスのname
プロパティは、エラーを識別するときにも使えますが、実はtoString()
を呼び出したときにも利用されます。
具体的には、以下のような違いがあります。
class NotFoundErrorWithName extends Error {
constructor(message: string) {
super(message);
this.name = 'NotFoundError';
}
}
class NotFoundErrorWithoutName extends Error {
constructor(message: string) {
super(message);
}
}
const errorWithName = new NotFoundErrorWithName('User not found');
console.log(errorWithName.toString());
// NotFoundError: User not found
const errorWithoutName = new NotFoundErrorWithoutName('User not found');
console.log(errorWithoutName.toString());
// Error: User not found
このように、name
プロパティを正しく設定しないとError: ...
のような出力になり、どの種類のエラーなのかが分かりづらくなってしまいます。
次に、カスタムエラーに追加の情報を持たせる例を見てみましょう。たとえばプロパティを追加したいときはこうなります。
class ValidationError extends Error {
constructor(public field: string, public value: unknown) {
super(`Validation failed: ${field}=${value}`);
this.name = 'ValidationError';
}
}
try {
throw new ValidationError('email', 'invalid-email');
} catch (error) {
if (error instanceof ValidationError) {
console.error(error.toString());
// ValidationError: Validation failed: email=invalid-email
}
}
このように、カスタムエラーに追加のフィールドを持たせることもできますが、コンストラクタが煩雑になり、ボイラープレートが増えていくのが現実です。
ライブラリを使ってボイラープレートを減らす
ここまでで、カスタムエラーを作る際の手間やボイラープレートの問題点が見えてきました。
これらの問題を解決するのが、@praha/error-factoryです。
このライブラリを使えば、わずか数行で型安全なカスタムエラーを定義できます。
基本的な使い方
まずは、npm
やpnpm
などお好みのパッケージマネージャーでライブラリインストールします。
npm install @praha/error-factory
次に、以下のようにしてカスタムエラーを定義します。
import { ErrorFactory } from '@praha/error-factory';
class NotFoundError extends ErrorFactory({
name: 'NotFoundError',
message: 'Resource not found',
}) {}
throw new NotFoundError();
ErrorFactory
関数は、クラスを返す高階関数で、name
やmessage
を設定したクラスを生成します。
これは、下記の記事の内容を応用した物になります。
cause
オプションのサポート
ECMAScript 2022から追加されたcause
オプションもサポートしています。
class DatabaseError extends ErrorFactory({
name: 'DatabaseError',
message: 'A database error occurred',
}) {}
throw new DatabaseError({ cause: new Error('Connection failed') });
このように、cause
オプションを使うことで、エラーの原因をチェーンすることでデバッグが容易になります。
高度な使い方
より高度な使い方として、動的なメッセージ生成や追加のプロパティを持たせることも可能です。
カスタムフィールド定義
次のように、ErrorFactory
のfields
メソッドを使って、追加のフィールドを定義できます。
class QueryError extends ErrorFactory({
name: 'QueryError',
message: 'An error occurred while executing a query',
fields: ErrorFactory.fields<{ query: string }>(),
}) {}
throw new QueryError({ query: 'SELECT * FROM users' });
動的なメッセージ生成
また、指定したフィールドを使って動的にメッセージを生成できます。
class ValidationError extends ErrorFactory({
name: 'ValidationError',
message: ({ field, value }) => `Validation failed for ${field}: ${value}`,
fields: ErrorFactory.fields<{ field: string; value: unknown }>(),
}) {}
throw new ValidationError({ field: 'email', value: 'invalid-email' });
パターンマッチング
ErrorFactory
に指定したname
は型ガードにも利用出来ます。
そのため、ts-pattern
などのパターンマッチングライブラリと組み合わせて使うこともできます。
import { ErrorFactory } from '@praha/error-factory';
import { match } from 'ts-pattern';
class NotFoundError extends ErrorFactory({
name: 'NotFoundError',
message: 'Resource not found',
}) {}
class ValidationError extends ErrorFactory({
name: 'ValidationError',
message: 'Validation failed',
}) {}
type ApplicationError = NotFoundError | ValidationError;
const handleError = (error: ApplicationError) => {
match(error)
.with({ name: 'NotFoundError' }, (e) => {
console.error('Handle NotFoundError:', e.toString());
})
.with({ name: 'ValidationError' }, (e) => {
console.error('Handle ValidationError:', e.toString());
})
.exhaustive();
};
まとめ
カスタムエラーを使うことで、エラーの種類を明確に区別し、追加情報を持たせることができ、コードの可読性と保守性が向上します。
しかし、手動でカスタムエラーを定義するとボイラープレートが増えてしまうことも事実です。
もしこの記事を読んで、同じようにカスタムエラーのボイラープレートに悩んでいる方は、ぜひ@praha/error-factory
を使ってみてください。
気に入ってもらえたら、GitHubのレポジトリにスターを付けていただけると嬉しいです。
その他にも、TypeScriptでの開発に役立つライブラリをいくつか公開していますので、ぜひチェックしてみてください!
Discussion