LoginSignup
40
23

More than 1 year has passed since last update.

TypeScriptのエラーハンドリングはResult型を使うのが良さそうだと思った話

Posted at

はじめに

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型のエラーオブジェクトで返しましょう。🤗🤗🤗

40
23
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
40
23