予期されるエラー
このガイドでは、以下のことを学びます:
- Effect が予期されるエラーをどのように表現するか
- 効果的で包括的なエラーマネジメントのために Effect が提供するツール
ガイドEffects の作成で見たように、failコンストラクタを使ってエラーを表現する Effect を作成できます:
import { Effect } from "effect";
class HttpError { readonly _tag = "HttpError";}
const program = Effect.fail(new HttpError());上記のHttpError型を表すためにクラスを使用したのは、エラー型と自由なコンストラクタにアクセスするためです。しかし、エラー型をモデル化するためにはお好みの方法を使用できます。
例の中で追加したreadonly _tagフィールドは、エラーの識別子としての役割を果たしています:
class HttpError { readonly _tag = "HttpError";}予期されるエラーは、“Error”チャネルにおいてEffectデータ型によって型レベルで追跡されます。
programの型から、エラーHttpErrorを伴って失敗する可能性があることが明らかです:
Effect<never, HttpError, never>;エラー追跡
次のプログラムは、エラーが自動的に追跡される方法を示す例です:
import { Effect, Random } from "effect"
export class HttpError { readonly _tag = "HttpError"}
export class ValidationError { readonly _tag = "ValidationError"}
export const program = Effect.gen(function* () { const n1 = yield* Random.next const n2 = yield* Random.next
const httpResult = n1 > 0.5 ? "yay!" : yield* Effect.fail(new HttpError()) const validationResult = n2 > 0.5 ? "yay!" : yield* Effect.fail(new ValidationError())
return httpResult + validationResult})// @include: error-tracking
Effect.runPromise(program).then(console.log, console.error);上記のプログラムでは、潜在的なエラー源を表すhttpResultとvalidationResultという 2 つの値を計算しています。
import { Effect, Random } from "effect";
export class HttpError { readonly _tag = "HttpError";}
export class ValidationError { readonly _tag = "ValidationError";}
const httpResult = Random.next.pipe( Effect.andThen((n1) => n1 > 0.5 ? Effect.succeed("yay!") : Effect.fail(new HttpError()) ));
const validationResult = Random.next.pipe( Effect.andThen((n2) => n2 > 0.5 ? Effect.succeed("yay!") : Effect.fail(new ValidationError()) ));
export const program = Effect.all([httpResult, validationResult]).pipe( Effect.andThen(([http, validation]) => http + validation));上記のプログラムでは、httpResultとvalidationResultという二つの操作があり、それぞれ潜在的なエラー源を表しています。
これらの操作は、Effect ライブラリのEffect.all(effects)関数を使用して組み合わせられ、順番に実行されます。
Effect はプログラムの実行中に発生する可能性のあるエラーを自動的に追跡します。
この場合、HttpErrorとValidationErrorが考えられるエラータイプです。
programのエラーチャネルは次のように指定されます:
Effect<string, HttpError | ValidationError, never>;これは、HttpErrorまたはValidationErrorのいずれかで失敗する可能性があることを示しています。
ショートサーキング
Effect.gen、Effect.map、Effect.flatMap、Effect.andThen、Effect.allなどの API を使用する際、エラーがどのように扱われるかを理解することが重要です。
これらの API は、最初のエラーに遭遇した際に実行をショートサーキットするように設計されています。
これは開発者にとってどういう意味を持つのでしょうか?たとえば、操作のチェーンや順番に実行される一連の効果があるとします。これらの効果の 1 つが実行中にエラーを発生させると、残りの計算はスキップされ、そのエラーが最終結果に伝播されます。
簡単に言えば、ショートサーキングの動作は、プログラムの任意のステップで問題が発生した場合、無駄な計算を実行する時間を浪費せず、直ちに停止してエラーを返すことを保証します。
import { Effect, Console } from "effect";
// さまざまなタスクを表す3つの効果を定義します。const task1 = Console.log("タスク1を実行中...");const task2 = Effect.fail("何かがうまくいきませんでした!");const task3 = Console.log("タスク3を実行中...");
// これら3つのタスクを順番に実行するように構成します。// もし1つのタスクが失敗すると、後続のタスクは実行されません。const program = Effect.gen(function* () { yield* task1; yield* task2; // task1の後にtask2が実行されるが、エラーで失敗する yield* task3; // 先行のタスクが失敗したため、これは実行されません});
Effect.runPromiseExit(program).then(console.log);/*出力:タスク1を実行中...{ _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Fail', failure: '何かがうまくいきませんでした!' }}*/import { Effect, Console } from "effect";
// さまざまなタスクを表す3つの効果を定義します。const task1 = Console.log("タスク1を実行中...");const task2 = Effect.fail("何かがうまくいきませんでした!");const task3 = Console.log("タスク3を実行中...");
// タスクを`Effect.andThen`を使用して順番に実行します。// `Effect.andThen`関数を使って、効果を連鎖できます。// もし1つのタスクが失敗すると、後続のタスクは実行されません。const program = task1.pipe( Effect.andThen(task2), // task1の後にtask2が実行されるが、エラーで失敗する Effect.andThen(task3) // 先行のタスクが失敗したため、これは実行されません);
Effect.runPromiseExit(program).then(console.log);/*出力:タスク1を実行中...{ _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Fail', failure: '何かがうまくいきませんでした!' }}*/このコードスニペットは、エラーが発生したときのショートサーキングの動作を示しています。
各操作は、前の操作が成功することに依存しています。
エラーが発生すると、実行はショートサーキットされ、エラーが伝播されます。
この特定の例では、task2でエラーが発生するため、task3は決して実行されません。
すべてのエラーをキャッチする
either
Effect.either関数は、Effect<A, E, R>を失敗と成功の両方をカプセル化したEitherデータ型に変換します:
Effect<A, E, R> -> Effect<Either<A, E>, never, R>結果の effect は失敗することができません。なぜなら、潜在的な失敗がEitherのLeft型内で表現されているからです。
返されるEffectのエラー型はneverとして指定されており、それにより効果が失敗しないように構成されていることが確認できます。
Eitherを使用して、生成関数内で失敗と成功の両方のケースを処理するために「パターンマッチ」ができるようになります。
// @include: error-tracking
// @filename: index.ts// ---cut---import { Effect, Either } from "effect";import { program } from "./error-tracking";
const recovered = Effect.gen(function* () { const failureOrSuccess = yield* Effect.either(program); if (Either.isLeft(failureOrSuccess)) { // 失敗ケース: `left`プロパティからエラーを抽出できます const error = failureOrSuccess.left; return `エラーから回復: ${error._tag}`; } else { // 成功ケース: `right`プロパティから値を抽出できます return failureOrSuccess.right; }});コードをより簡潔にするために、エラーハンドリングと成功値の 2 つのコールバック関数を直接受け取るEither.match関数を使用できます:
// @include: error-tracking
// @filename: index.ts// ---cut---import { Effect, Either } from "effect";import { program } from "./error-tracking";
const recovered = Effect.gen(function* () { const failureOrSuccess = yield* Effect.either(program); return Either.match(failureOrSuccess, { onLeft: (error) => `エラーから回復: ${error._tag}`, onRight: (value) => value, // 成功時は何もしない });});catchAll
Effect.catchAll関数を使用すると、プログラム内で発生したいかなるエラーもキャッチし、フォールバックを提供することができます。
// @include: error-tracking
// @filename: index.ts// ---cut---import { Effect } from "effect";import { program } from "./error-tracking";
const recovered = program.pipe( Effect.catchAll((error) => Effect.succeed(`エラーから回復: ${error._tag}`)));私たちのプログラムのエラーチャネルの型がneverに変わったことに注目できます。これは、すべてのエラーが処理されたことを示しています。
一部のエラーをキャッチする
例えば、特定のエラーHttpErrorを処理したいとします。
// @include: error-tracking
// @filename: index.ts// ---cut---import { Effect, Either } from "effect";import { program } from "./error-tracking";
const recovered = Effect.gen(function* () { const failureOrSuccess = yield* Effect.either(program); if (Either.isLeft(failureOrSuccess)) { const error = failureOrSuccess.left; if (error._tag === "HttpError") { return "HttpErrorから回復"; } return yield* Effect.fail(error); } else { return failureOrSuccess.right; }});私たちのプログラムのエラーチャネルの型がValidationErrorだけを示すように変わり、HttpErrorが処理されたことを示しています。
ValidationErrorも処理したい場合、私たちは簡単に別のケースをコードに追加できます:
// @include: error-tracking
// @filename: index.ts// ---cut---import { Effect, Either } from "effect";import { program } from "./error-tracking";
const recovered = Effect.gen(function* () { const failureOrSuccess = yield* Effect.either(program); if (Either.isLeft(failureOrSuccess)) { const error = failureOrSuccess.left; if (error._tag === "HttpError") { return "HttpErrorから回復"; } else { return "ValidationErrorから回復"; } } else { return failureOrSuccess.right; }});私たちのプログラムのエラーチャネルの型がneverに変わったことに注目できます。これは、すべてのエラーが処理されたことを示しています。
catchSome
特定の種類のエラーをキャッチして回復し、効果的に回復を試みたい場合は、Effect.catchSome関数を使用できます:
// @include: error-tracking
// @filename: index.ts// ---cut---import { Effect, Option } from "effect";import { program } from "./error-tracking";
const recovered = program.pipe( Effect.catchSome((error) => { if (error._tag === "HttpError") { return Option.some(Effect.succeed("HttpErrorから回復")); } return Option.none(); }));上記のコードでは、Effect.catchSomeはエラー(error)を調べ、そのエラーに対して回復を試みるかどうかを決定します。エラーが特定の条件に一致する場合、Option.some(effect)を返すことで回復を試みます。回復が不可能な場合は、単にOption.none()を返します。
Effect.catchSomeは特定のエラーをキャッチできますが、エラーの型自体は変更しないことに注意することが重要です。したがって、結果の効果(この場合はrecovered)は、元の効果と同じエラー型(HttpError | ValidationError)を持ち続けます。
catchIf
Effect.catchSomeに似て、Effect.catchIf関数を使用して、述語に基づいて特定のエラーから回復することができます:
// @include: error-tracking
// @filename: index.ts// ---cut---import { Effect } from "effect";import { program } from "./error-tracking";
const recovered = program.pipe( Effect.catchIf( (error) => error._tag === "HttpError", () => Effect.succeed("HttpErrorから回復") ));TypeScript バージョン< 5.5 において、Effect.catchIfは特定のエラーをキャッチできますが、エラーの型自体は変更しません。したがって、結果の効果(この場合はrecovered)は、元の効果と同じエラー型(HttpError | ValidationError)を持ち続けます。TypeScript バージョン>= 5.5 では、改善された型絞り込みにより、結果のエラー型がValidationErrorとして推論されます。
TypeScript バージョン< 5.5 の場合、述語の代わりにユーザー定義の型ガードを提供すると、結果のエラー型がプルーニングされ、Effect<string, ValidationError, never>が返されます:
// @include: error-tracking
// @filename: index.ts// ---cut---import { Effect } from "effect";import { program, HttpError } from "./error-tracking";
const recovered = program.pipe( Effect.catchIf( (error): error is HttpError => error._tag === "HttpError", () => Effect.succeed("HttpErrorから回復") ));catchTag
プログラムのエラーがすべて識別子として動作する_tagフィールドでタグ付けされている場合、特定のエラーを正確にキャッチして処理できるEffect.catchTag関数を使用できます。
// @include: error-tracking
// @filename: index.ts// ---cut---import { Effect } from "effect";import { program } from "./error-tracking";
const recovered = program.pipe( Effect.catchTag("HttpError", (_HttpError) => Effect.succeed("HttpErrorから回復") ));上記の例では、Effect.catchTag関数を使用してHttpErrorを具体的に処理できるようにします。
プログラムの実行中にHttpErrorが発生した場合、提供されたエラーハンドラ関数が呼び出され、そのハンドラ内で指定された回復ロジックが実行されます。
私たちのプログラムのエラーチャネルの型がValidationErrorのみを示すように変わり、HttpErrorが処理されたことを示しています。
ValidationErrorも処理したい場合、単に別のcatchTagを追加するだけです:
// @include: error-tracking
// @filename: index.ts// ---cut---import { Effect } from "effect";import { program } from "./error-tracking";
const recovered = program.pipe( Effect.catchTag("HttpError", (_HttpError) => Effect.succeed("HttpErrorから回復") ), Effect.catchTag("ValidationError", (_ValidationError) => Effect.succeed("ValidationErrorから回復") ));私たちのプログラムのエラーチャネルの型がneverに変わったことに注目できます。これは、すべてのエラーが処理されたことを示しています。
catchTags
個々のエラー型を処理するためにEffect.catchTag関数を複数回使用する代わりに、Effect.catchTagsというより便利なオプションを使用できます。この関数を使用すると、1 つのコードブロックで複数のエラーを処理できます。
// @include: error-tracking
// @filename: index.ts// ---cut---import { Effect } from "effect";import { program } from "./error-tracking";
const recovered = program.pipe( Effect.catchTags({ HttpError: (_HttpError) => Effect.succeed(`HttpErrorから回復`), ValidationError: (_ValidationError) => Effect.succeed(`ValidationErrorから回復`), }));上記の例では、個々のエラーを処理するためにEffect.catchTagを複数回使用するのではなく、Effect.catchTagsコンビネータを利用します。
このコンビネータは、各プロパティが特定のエラー_tag(この場合は"HttpError"と"ValidationError")を表すオブジェクトを受け取り、対応する値がその特定のエラーが発生したときに実行されるエラーハンドラ関数です。