はじめに
TypeScriptのエラーハンドリングの方法について、なにか良い方法はないかと探していたら、以下記事を見つけた。
この記事に書かれている方法を取り入れてコードを書いてみたら、個人的にとてもスッキリしたので、布教したい。😭
Result型を使うと例外スローの苦しみから開放される
この方法の何が良いのかを説明すると、(といっても、上記記事に書いてある通りなのだが)標準的な「例外をスローさせる」方法だと以下に苦しみを感じていた。
- 型安全でない(型から例外がスローされるかわからない)
- 扱いづらい(スローされた例外は、catchして扱わなくてはならない)
- テストが辛い(Jestで言えば、
toThrow()
,toThrowError()
等を使って評価しなくてはならない)
これらの苦しみは、Result型を使って「エラーオブジェクトを返す」と全てクリアになる。
Result型の使い方
まずはResult型を作る。
type Result<T, E extends Error> = Success<T> | Failure<E>;
class Success<T> {
readonly value: T;
constructor(value: T) {
this.value = value;
}
isSuccess(): this is Success<T> {
return true;
}
isFailure(): this is Failure<Error> {
return false;
}
}
class Failure<E extends Error> {
readonly error: E;
constructor(error: E) {
this.error = error;
}
isSuccess(): this is Success<unknown> {
return false;
}
isFailure(): this is Failure<E> {
return true;
}
}
次にエラーレスポンスを返す形式も決めておく。
たとえば、エラーが発生した時に必要な情報をオブジェクトに集約させておくと便利。
Errorオブジェクトに対して以下を追加しておくと、フロントとのエラーの連携や追跡に役立つ。
- functionName ⇒ エラーが発生した関数名
- statusCode ⇒ HTTPのステータスコード
- code ⇒ 開発者側で認識するエラーコード
class ErrorResponse extends Error {
readonly name: string
readonly message: string
readonly stack?: string
constructor(
readonly functionName: string = 'unknown functionName',
readonly statusCode: number = 500,
readonly code: string= 'APP_UNKNOWN_ERROR',
readonly error: Error = new Error('unknown error')) {
super()
this.name = error.name
this.message = error.message
this.stack = error.stack
}
}
自身で作成した関数であれば、以下のように new Failure()
を返り値に設定すれば良い。
function foo(m: string): Result<string, ErrorResponse> {
if (m === 'm') {
return new Success('success')
} else {
return new Failure(new ErrorResponse(foo.name, 500, 'APP_FOO_FUNCTION_ERROR'))
}
}
なにかしらサードパーティ製ライブラリをつかった実装で new Failure()
を返り値に設定したい場合は、 try-catch
を使って new Failure()
を返り値に設定する。
function bar(m: string): Result<string, ErrorResponse> {
try {
// なにかしらサードパーティ製ライブラリをつかった実装
} catch(err) {
if (err instanceof Error) {
return new Failure(new ErrorResponse(bar.name, 500, 'APP_ERROR', err))
}
return new Failure(new ErrorResponse(bar.name))
}
}
こうしてエラー発生時にオブジェクトを返すことで、テストにおいてもエラー用のマッチャー(toThrow()
, toThrowError()
)を使用しなくてよくなり、 toBe()
や toEqual
を使用して評価することができる。
const result = foo('a')
expect(result.isFailure() ? result.error.statusCode : undefined).toBe(500)
expect(result.isFailure() ? result.error.code : undefined).toBe('APP_FOO_FUNCTION_ERROR')
おわりに
try-catch
を使って例外をスローさせる方法は、人類には早すぎたのではないだろうか🤔🤔🤔
例外はResult型のエラーオブジェクトで返しましょう。🤗🤗🤗