TypeScript Effectシステムエラー処理実践:関数型エラー管理の5つのコアパターン
2026年、TypeScriptエコシステムの関数型プログラミングパラダイムがエラー処理のあり方を変えつつあります。Effect-TSはTypeScript最強のEffectシステムとして、型安全なエラー合成、タグ付きエラー、宣言的エラー回復により、「エラー処理の忘れを不可能にする」ことを現実にしました。従来のtry-catchやPromise.catchと異なり、Effectシステムはエラー型を関数シグネチャにエンコードし、コンパイラがすべての可能なエラーの処理を強制します。本記事では実践的な観点から、TypeScript Effectシステムエラー処理の5つのコアパターンを深く掘り下げ、堅牢な本番級アプリケーションの構築方法を解説します。
コア概念
| 概念 | 説明 | 適用シナリオ |
|---|---|---|
| Effect | 計算を記述する遅延値、エラーを含む可能性あり | すべての副作用操作 |
| Effect.Success | 成功のEffect | 純粋計算 |
| Effect.Failure | 失敗のEffect | エラー伝播 |
| Tagged Error | タグ付きエラー型 | エラー分類とマッチング |
| Layer | 依存性注入レイヤー | サービス依存管理 |
| Schedule | リトライ戦略スケジューラ | エラー回復 |
| Cause | エラー原因追跡 | エラー診断 |
問題分析:5つのペインポイント
- エラー型の消失:Promiseのcatchはunknown型しか取得できず、実行時までエラーの内容が不明。コンパイラが未処理エラーをチェックできず、本番で頻繁に"undefined is not a function"が発生
- エラー処理の不統一:チーム内でtry-catch、.catch()、Resultパターン、直接throwなどが混在し、エラー処理スタイルが混乱。新規メンバーがどう書けばいいか分からない
- エラー回復の困難さ:ネットワークリクエスト失敗時のリトライ、DB接続断絶時のフェイルオーバーが必要だが、try-catchが3層ネストするとコードが読めなくなる
- エラー合成の爆発:関数AがエラーE1を、関数BがE2を投げる可能性がある場合、合成後はE1|E2、さらに合成するとE1|E2|E3|E4...となり型推論が制御不能に
- エラーログのコンテキスト欠落:エラーをcatchしてもerror.messageしか記録されず、コールチェーン、パラメータ、タイミングなどのコンテキストが欠落。本番トラブルシューティングで「エラーメッセージが理解できず、スタックトレースから根本原因が見つからない」
パターン1:Effect-TS基礎と型安全エラー
Effectのコア思想:エラーは型システムの一部であり、ランタイムの予期せぬ事態ではない。
// src/effects/basic.ts - Effect基礎
import { Effect } from "effect";
// ====== 従来の方法 vs Effectアプローチ ======
// ❌ 従来:エラー型が消失
async function fetchUserTraditional(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`Failed to fetch user: ${response.status}`);
// 呼び出し側はこの関数がエラーを投げる可能性を知らない!
}
return response.json();
}
// ✅ Effect:エラーを型にエンコード
class FetchError {
readonly _tag = "FetchError";
constructor(readonly status: number, readonly url: string) {}
}
class ParseError {
readonly _tag = "ParseError";
constructor(readonly message: string, readonly raw: unknown) {}
}
const fetchUser = (id: string): Effect.Effect<User, FetchError | ParseError, never> =>
Effect.gen(function* (_) {
const url = `/api/users/${id}`;
const response = yield* _(
Effect.tryPromise({
try: () => fetch(url),
catch: (error) => new FetchError(0, url),
})
);
if (!response.ok) {
yield* _(Effect.fail(new FetchError(response.status, url)));
}
const data = yield* _(
Effect.try({
try: () => response.json() as Promise<User>,
catch: (error) => new ParseError("Failed to parse JSON", error),
})
);
return data;
});
// 型シグネチャが明確に示す:このEffectは失敗する可能性があり、エラー型はFetchError | ParseError
// コンパイラがこれらのエラーの処理を強制する!
// ====== 基本Effect操作 ======
// 成功値
const success: Effect.Effect<number, never, never> = Effect.succeed(42);
// 失敗値
const failure: Effect.Effect<never, string, never> = Effect.fail("something went wrong");
// 同期計算から作成
const fromSync = Effect.sync(() => {
const value = Math.random();
return value > 0.5 ? value : -1;
});
// Promiseから作成
const fromPromise = Effect.tryPromise({
try: () => fetch("https://api.toolsku.com/health"),
catch: (error) => new FetchError(0, "https://api.toolsku.com/health"),
});
// ====== Effectの実行 ======
import { Runtime } from "effect";
const program = fetchUser("user-123");
// 方法1:デフォルトRuntimeで実行
const result1 = await Effect.runPromise(program);
// 方法2:安全に実行し、Exitを取得
const result2 = await Effect.runPromiseExit(program);
if (result2._tag === "Success") {
console.log("User:", result2.value);
} else {
console.log("Error:", result2.cause);
}
// 方法3:カスタムRuntime(本番推奨)
interface AppEnv {
readonly apiBaseUrl: string;
readonly timeout: number;
}
const AppLive = Runtime.defaultRuntime;
const customProgram = Effect.gen(function* (_) {
const env = yield* _(Effect.service(AppEnv));
const url = `${env.apiBaseUrl}/users/123`;
});
// src/effects/user-service.ts - 完全なユーザーサービスEffect
import { Effect, Context, Layer } from "effect";
// ====== エラー型の定義 ======
class UserNotFoundError {
readonly _tag = "UserNotFoundError";
constructor(readonly userId: string) {}
}
class DatabaseError {
readonly _tag = "DatabaseError";
constructor(
readonly operation: string,
readonly cause: unknown,
) {}
}
class ValidationError {
readonly _tag = "ValidationError";
constructor(
readonly field: string,
readonly message: string,
) {}
}
// ====== サービスインターフェースの定義 ======
interface UserRepository {
readonly findById: (id: string) => Effect.Effect<User, UserNotFoundError | DatabaseError, never>;
readonly create: (data: CreateUserInput) => Effect.Effect<User, ValidationError | DatabaseError, never>;
readonly update: (id: string, data: UpdateUserInput) => Effect.Effect<User, UserNotFoundError | ValidationError | DatabaseError, never>;
readonly delete: (id: string) => Effect.Effect<void, UserNotFoundError | DatabaseError, never>;
}
const UserRepository = Context.GenericTag<UserRepository>("UserRepository");
// ====== ビジネスロジックの実装 ======
const getUser = (id: string): Effect.Effect<User, UserNotFoundError | DatabaseError, UserRepository> =>
Effect.gen(function* (_) {
const repo = yield* _(UserRepository);
return yield* _(repo.findById(id));
});
const createUser = (input: CreateUserInput): Effect.Effect<User, ValidationError | DatabaseError, UserRepository> =>
Effect.gen(function* (_) {
yield* _(validateCreateUserInput(input));
const repo = yield* _(UserRepository);
return yield* _(repo.create(input));
});
const validateCreateUserInput = (input: CreateUserInput): Effect.Effect<void, ValidationError, never> =>
Effect.gen(function* (_) {
if (!input.email || !input.email.includes("@")) {
yield* _(Effect.fail(new ValidationError("email", "Invalid email format")));
}
if (!input.name || input.name.length < 2) {
yield* _(Effect.fail(new ValidationError("name", "Name must be at least 2 characters")));
}
if (input.age !== undefined && (input.age < 0 || input.age > 150)) {
yield* _(Effect.fail(new ValidationError("age", "Age must be between 0 and 150")));
}
});
interface User {
id: string;
name: string;
email: string;
age: number;
createdAt: string;
updatedAt: string;
}
interface CreateUserInput {
name: string;
email: string;
age?: number;
}
interface UpdateUserInput {
name?: string;
email?: string;
age?: number;
}
パターン2:タグ付きエラー(Tagged Error)とエラー分類
タグ付きエラーはEffectシステムの魂です。_tagフィールドにより、型レベルで異なるエラー型を正確にマッチング・処理できます。
// src/effects/tagged-errors.ts - タグ付きエラーと分類
import { Effect, Match } from "effect";
// ====== ドメインエラー階層の定義 ======
interface DomainError {
readonly _tag: string;
readonly timestamp: string;
readonly traceId: string;
}
class UnauthorizedError implements DomainError {
readonly _tag = "UnauthorizedError";
readonly timestamp = new Date().toISOString();
constructor(
readonly traceId: string,
readonly reason: "expired_token" | "invalid_token" | "no_token",
) {}
}
class ForbiddenError implements DomainError {
readonly _tag = "ForbiddenError";
readonly timestamp = new Date().toISOString();
constructor(
readonly traceId: string,
readonly resource: string,
readonly action: string,
) {}
}
class InsufficientBalanceError implements DomainError {
readonly _tag = "InsufficientBalanceError";
readonly timestamp = new Date().toISOString();
constructor(
readonly traceId: string,
readonly required: number,
readonly available: number,
) {}
}
class DuplicateResourceError implements DomainError {
readonly _tag = "DuplicateResourceError";
readonly timestamp = new Date().toISOString();
constructor(
readonly traceId: string,
readonly resourceType: string,
readonly identifier: string,
) {}
}
class DatabaseConnectionError implements DomainError {
readonly _tag = "DatabaseConnectionError";
readonly timestamp = new Date().toISOString();
constructor(
readonly traceId: string,
readonly host: string,
readonly cause: unknown,
) {}
}
class CacheError implements DomainError {
readonly _tag = "CacheError";
readonly timestamp = new Date().toISOString();
constructor(
readonly traceId: string,
readonly operation: string,
readonly cause: unknown,
) {}
}
// ====== Matchによる型安全なエラー処理 ======
type AppError =
| UnauthorizedError
| ForbiddenError
| InsufficientBalanceError
| DuplicateResourceError
| DatabaseConnectionError
| CacheError;
const handleAppError = (error: AppError): HttpResponse => {
return Match.value(error).pipe(
Match.tag("UnauthorizedError", (err) => ({
status: 401,
body: {
code: "UNAUTHORIZED",
message: `Authentication failed: ${err.reason}`,
traceId: err.traceId,
},
})),
Match.tag("ForbiddenError", (err) => ({
status: 403,
body: {
code: "FORBIDDEN",
message: `Cannot ${err.action} on ${err.resource}`,
traceId: err.traceId,
},
})),
Match.tag("InsufficientBalanceError", (err) => ({
status: 409,
body: {
code: "INSUFFICIENT_BALANCE",
message: `Required ${err.required}, available ${err.available}`,
traceId: err.traceId,
},
})),
Match.tag("DuplicateResourceError", (err) => ({
status: 409,
body: {
code: "DUPLICATE_RESOURCE",
message: `${err.resourceType} '${err.identifier}' already exists`,
traceId: err.traceId,
},
})),
Match.tag("DatabaseConnectionError", (err) => ({
status: 503,
body: {
code: "SERVICE_UNAVAILABLE",
message: "Database temporarily unavailable",
traceId: err.traceId,
},
})),
Match.tag("CacheError", (err) => ({
status: 503,
body: {
code: "CACHE_ERROR",
message: "Cache service error",
traceId: err.traceId,
},
})),
Match.exhaustive, // コンパイラがすべてのエラー型の処理を保証!
);
};
// ====== エラー分類ユーティリティ ======
const isRetryable = (error: AppError): boolean => {
switch (error._tag) {
case "DatabaseConnectionError":
case "CacheError":
return true; // インフラエラーはリトライ可能
case "UnauthorizedError":
case "ForbiddenError":
case "InsufficientBalanceError":
case "DuplicateResourceError":
return false; // ビジネスエラーはリトライ不可
}
};
const isErrorSeverity = (error: AppError): "low" | "medium" | "high" | "critical" => {
switch (error._tag) {
case "CacheError":
return "low";
case "UnauthorizedError":
case "DuplicateResourceError":
return "medium";
case "ForbiddenError":
case "InsufficientBalanceError":
return "high";
case "DatabaseConnectionError":
return "critical";
}
};
パターン3:エラー回復とリトライ戦略
EffectのScheduleシステムは宣言的なリトライ戦略を提供し、エラー回復コードを強力かつ読みやすくします。
// src/effects/recovery.ts - エラー回復とリトライ
import { Effect, Schedule, Duration } from "effect";
// ====== 基本リトライ戦略 ======
// 単純に3回リトライ
const withSimpleRetry = <A, E, R>(
effect: Effect.Effect<A, E, R>,
): Effect.Effect<A, E, R> =>
effect.pipe(Effect.retry(Schedule.recurs(3)));
// 指数バックオフリトライ
const withExponentialBackoff = <A, E, R>(
effect: Effect.Effect<A, E, R>,
): Effect.Effect<A, E, R> =>
effect.pipe(
Effect.retry(
Schedule.exponential(Duration.millis(100), 2.0).pipe(
Schedule.jittered,
Schedule.whileOutput(Duration.lessThan(Duration.seconds(30))),
Schedule.compose(Schedule.recurs(5)),
)
)
);
// エラー型に基づくスマートリトライ
const withSmartRetry = <A, E extends { readonly _tag: string }, R>(
effect: Effect.Effect<A, E, R>,
): Effect.Effect<A, E, R> =>
effect.pipe(
Effect.retry(
Schedule.whileInput((error: E) => {
return error._tag === "DatabaseConnectionError" || error._tag === "CacheError";
}).pipe(
Schedule.compose(
Schedule.exponential(Duration.millis(200), 2.0).pipe(
Schedule.jittered,
Schedule.compose(Schedule.recurs(5)),
)
)
)
)
);
// ====== フォールバック戦略 ======
const withFallback = <A, E1, E2, R1, R2>(
primary: Effect.Effect<A, E1, R1>,
fallback: Effect.Effect<A, E2, R2>,
): Effect.Effect<A, E1 | E2, R1 | R2> =>
Effect.catchAll(primary, () => fallback);
// キャッシュベースのフォールバック
const withCacheFallback = <A, E, R>(
primary: Effect.Effect<A, E, R>,
cacheKey: string,
): Effect.Effect<A, E, R, R | CacheService> =>
Effect.gen(function* (_) {
const cache = yield* _(CacheService);
const result = yield* _(
primary.pipe(
Effect.tap((value) => cache.set(cacheKey, value, Duration.minutes(10))),
Effect.catchAll(() =>
Effect.gen(function* (_) {
const cached = yield* _(cache.get<A>(cacheKey));
if (cached) {
yield* _(Effect.logWarning(`Using cached value for ${cacheKey}`));
return cached;
}
return yield* _(primary);
})
),
)
);
return result;
});
// ====== タイムアウトとレース ======
const withTimeout = <A, E, R>(
effect: Effect.Effect<A, E, R>,
duration: Duration.Duration,
): Effect.Effect<A, E | TimeoutError, R> =>
effect.pipe(
Effect.timeout(duration),
Effect.mapEffect(
(option) =>
option._tag === "Some"
? Effect.succeed(option.value)
: Effect.fail(new TimeoutError(Duration.toMillis(duration)))
),
);
const race = <A, E1, E2, R1, R2>(
effect1: Effect.Effect<A, E1, R1>,
effect2: Effect.Effect<A, E2, R2>,
): Effect.Effect<A, E1 | E2, R1 | R2> =>
Effect.race(effect1, effect2);
interface CacheService {
readonly get: <A>(key: string) => Effect.Effect<A | null, never, never>;
readonly set: <A>(key: string, value: A, ttl: Duration.Duration) => Effect.Effect<void, never, never>;
}
const CacheService = Context.GenericTag<CacheService>("CacheService");
class TimeoutError {
readonly _tag = "TimeoutError";
constructor(readonly durationMs: number) {}
}
パターン4:エラー合成とLayer依存
複数のEffectを合成すると、エラー型は自動的にマージされます。Layerシステムはサービスの依存関係を管理します。
// src/effects/composition.ts - エラー合成とLayer
import { Effect, Context, Layer } from "effect";
// ====== エラー合成 ======
const transferMoney = (
fromUserId: string,
toUserId: string,
amount: number,
): Effect.Effect<
TransferResult,
| UserNotFoundError
| InsufficientBalanceError
| DatabaseError
| UnauthorizedError,
UserRepository | AuditService | NotificationService
> =>
Effect.gen(function* (_) {
const repo = yield* _(UserRepository);
const audit = yield* _(AuditService);
const notification = yield* _(NotificationService);
const [fromUser, toUser] = yield* _(
Effect.all([
repo.findById(fromUserId),
repo.findById(toUserId),
])
);
if (fromUser.balance < amount) {
yield* _(Effect.fail(
new InsufficientBalanceError(generateTraceId(), amount, fromUser.balance)
));
}
const result = yield* _(
repo.transfer(fromUserId, toUserId, amount).pipe(
Effect.tap((result) =>
audit.logTransfer(fromUserId, toUserId, amount, result.transactionId).pipe(
Effect.catchAll((error) =>
Effect.logError(`Audit log failed: ${error}`)
)
)
),
Effect.tap((result) =>
notification.notifyTransfer(fromUser, toUser, amount, result.transactionId).pipe(
Effect.catchAll((error) =>
Effect.logError(`Notification failed: ${error}`)
)
)
),
)
);
return result;
});
// ====== エラー隔離 ======
const transferMoneyApi = (
fromUserId: string,
toUserId: string,
amount: number,
): Effect.Effect<TransferResult, ApiError, UserRepository | AuditService | NotificationService> =>
transferMoney(fromUserId, toUserId, amount).pipe(
Effect.catchAll((error): Effect.Effect<never, ApiError, never> => {
switch (error._tag) {
case "UserNotFoundError":
return Effect.fail(new ApiError(404, "USER_NOT_FOUND", error.userId));
case "InsufficientBalanceError":
return Effect.fail(new ApiError(409, "INSUFFICIENT_BALANCE", `Required ${error.required}`));
case "DatabaseError":
return Effect.fail(new ApiError(503, "SERVICE_UNAVAILABLE", "Database error"));
case "UnauthorizedError":
return Effect.fail(new ApiError(401, "UNAUTHORIZED", error.reason));
}
})
);
// ====== Layer依存管理 ======
const AuditService = Context.GenericTag<AuditService>("AuditService");
const NotificationService = Context.GenericTag<NotificationService>("NotificationService");
const AuditServiceLive = Layer.succeed(AuditService, {
logTransfer: (from, to, amount, txId) =>
Effect.gen(function* (_) {
yield* _(Effect.logInfo(`Audit: ${from} -> ${to}, amount: ${amount}, txId: ${txId}`));
}),
});
const NotificationServiceLive = Layer.succeed(NotificationService, {
notifyTransfer: (from, to, amount, txId) =>
Effect.gen(function* (_) {
yield* _(Effect.logInfo(`Notify: transfer ${amount} from ${from.name} to ${to.name}`));
}),
});
const AppLayer = Layer.mergeAll(
AuditServiceLive,
NotificationServiceLive,
);
パターン5:本番級エラー管理とログ統合
本番環境では、エラー管理をログ、監視、アラートシステムと深く統合する必要があります。
// src/effects/production.ts - 本番級エラー管理
import { Effect, Fiber, Metric, Schedule, Duration } from "effect";
// ====== エラーログミドルウェア ======
const withErrorLogging = <A, E extends { readonly _tag: string }, R>(
effect: Effect.Effect<A, E, R>,
context: Record<string, unknown> = {},
): Effect.Effect<A, E, R> =>
effect.pipe(
Effect.tapError((error) =>
Effect.gen(function* (_) {
yield* _(Effect.logError("Operation failed", {
errorTag: error._tag,
error: JSON.stringify(error, null, 2),
context,
timestamp: new Date().toISOString(),
fiberId: Fiber.currentFiberId(),
}));
})
),
);
// ====== エラーメトリクス収集 ======
const errorCounter = Metric.counter("app_errors_total");
const errorRateByType = Metric.keyValue("app_errors_by_type", Metric.counter);
const withErrorMetrics = <A, E extends { readonly _tag: string }, R>(
effect: Effect.Effect<A, E, R>,
): Effect.Effect<A, E, R> =>
effect.pipe(
Effect.tapError((error) =>
Effect.gen(function* (_) {
yield* _(Metric.increment(errorCounter));
yield* _(Metric.increment(errorRateByType(error._tag)));
})
),
);
// ====== エラーレポート ======
interface ErrorReport {
readonly id: string;
readonly errorTag: string;
readonly message: string;
readonly stack?: string;
readonly context: Record<string, unknown>;
readonly timestamp: string;
readonly traceId: string;
readonly severity: "low" | "medium" | "high" | "critical";
readonly retryable: boolean;
}
const createErrorReport = <E extends DomainError>(
error: E,
context: Record<string, unknown> = {},
): ErrorReport => ({
id: crypto.randomUUID(),
errorTag: error._tag,
message: JSON.stringify(error),
context,
timestamp: error.timestamp,
traceId: error.traceId,
severity: isErrorSeverity(error),
retryable: isRetryable(error),
});
// ====== グローバルエラーハンドラー ======
const globalErrorHandler = <A, E extends DomainError>(
effect: Effect.Effect<A, E, never>,
): Effect.Effect<A, never, never> =>
effect.pipe(
withErrorLogging,
withErrorMetrics,
Effect.catchAll((error: E) =>
Effect.gen(function* (_) {
const report = createErrorReport(error);
yield* _(
Effect.tryPromise({
try: () => fetch("https://sentry.io/api/12345/store/", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
event_id: report.id,
message: `[${report.errorTag}] ${report.message}`,
extra: report.context,
tags: {
errorTag: report.errorTag,
severity: report.severity,
retryable: String(report.retryable),
},
timestamp: report.timestamp,
}),
}),
catch: () => new Error("Failed to report to Sentry"),
}).pipe(
Effect.catchAll(() => Effect.logWarning("Failed to report error to Sentry")),
)
);
if (report.severity === "critical") {
yield* _(sendAlert(report));
}
return yield* _(Effect.succeed(getFallbackResponse(error) as A));
})
),
);
// ====== エラーバウンダリー ======
const errorBoundary = <A, E extends DomainError, R>(
effect: Effect.Effect<A, E, R>,
options: {
readonly fallback?: A;
readonly maxRetries?: number;
readonly retryDelay?: Duration.Duration;
readonly onError?: (error: E) => void;
},
): Effect.Effect<A, E, R> =>
effect.pipe(
Effect.retry(
Schedule.exponential(options.retryDelay ?? Duration.millis(100), 2.0).pipe(
Schedule.jittered,
Schedule.whileInput((error: E) => isRetryable(error)),
Schedule.compose(Schedule.recurs(options.maxRetries ?? 3)),
)
),
options.fallback
? Effect.catchAll(() => Effect.succeed(options.fallback!))
: Effect.catchAll((error) => {
options.onError?.(error);
return Effect.fail(error);
}),
);
// ====== HTTPハンドラー統合 ======
const makeHttpHandler = <A, E extends DomainError, R>(
effect: Effect.Effect<A, E, R>,
) => {
return async (request: Request): Promise<Response> => {
const program = effect.pipe(
withErrorLogging({ path: new URL(request.url).pathname }),
withErrorMetrics,
Effect.map((data) => Response.json({ success: true, data })),
Effect.catchAll((error: E) =>
Effect.succeed(
Response.json(
{ success: false, error: createErrorReport(error) },
{ status: getHttpStatus(error) }
)
)
),
);
return Effect.runPromise(program);
};
};
function getHttpStatus(error: DomainError): number {
switch (error._tag) {
case "UnauthorizedError": return 401;
case "ForbiddenError": return 403;
case "UserNotFoundError": return 404;
case "InsufficientBalanceError":
case "DuplicateResourceError": return 409;
case "ValidationError": return 422;
case "DatabaseConnectionError":
case "CacheError": return 503;
default: return 500;
}
}
function generateTraceId(): string {
return `trace-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
}
const sendAlert = (report: ErrorReport): Effect.Effect<void, never, never> =>
Effect.logCritical(`🚨 CRITICAL ALERT: ${report.errorTag} - ${report.message}`);
function getFallbackResponse(error: DomainError): unknown {
return { status: "degraded", errorTag: error._tag };
}
よくある落とし穴
落とし穴1:Effectの実行忘れ
// ❌ 間違い:Effectは遅延評価、実行しないと何も起こらない
const fetchUser = (id: string) =>
Effect.gen(function* (_) {
const response = yield* _(Effect.tryPromise(() => fetch(`/api/users/${id}`)));
return response;
});
fetchUser("123"); // 何も起こらない!
// ✅ 正しい:Effectを明示的に実行する
const program = fetchUser("123");
const result = await Effect.runPromise(program);
落とし穴2:Effect内でのthrow使用
// ❌ 間違い:Effect内でthrowを使うとエラー型システムをバイパスする
const parseJson = (input: string): Effect.Effect<unknown, never, never> =>
Effect.sync(() => JSON.parse(input)); // throwはDefectになり、型に含まれない!
// ✅ 正しい:Effect.tryまたはEffect.failを使用
const parseJson = (input: string): Effect.Effect<unknown, ParseError, never> =>
Effect.try({
try: () => JSON.parse(input),
catch: (error) => new ParseError("JSON parse failed", error),
});
落とし穴3:エラー型不一致によるcatchAll失敗
// ❌ 間違い:catchAllはすべてのエラー型を処理する必要がある
const program = Effect.gen(function* (_) {
const user = yield* _(fetchUser("123")); // エラー: FetchError | ParseError
}).pipe(
Effect.catchAll((error: FetchError) => // ❌ ParseErrorが未処理!
Effect.succeed(null)
)
);
// ✅ 正しい:Matchを使用してすべてのエラー型を処理
const program = Effect.gen(function* (_) {
const user = yield* _(fetchUser("123"));
}).pipe(
Effect.catchAll((error: FetchError | ParseError) =>
Match.value(error).pipe(
Match.tag("FetchError", () => Effect.succeed(null)),
Match.tag("ParseError", () => Effect.succeed(null)),
Match.exhaustive,
)
)
);
落とし穴4:Layer未提供によるランタイムエラー
// ❌ 間違い:UserRepositoryを使用しているがLayerが未提供
const program = getUser("123");
await Effect.runPromise(program); // ランタイムエラー:Missing service: UserRepository
// ✅ 正しい:Layerを提供してから実行
const program = getUser("123").pipe(
Effect.provide(UserRepositoryLive),
);
await Effect.runPromise(program);
落とし穴5:gen関数でのyield*忘れ
// ❌ 間違い:yield*を忘れるとEffectが実行されない
const program = Effect.gen(function* (_) {
const user = fetchUser("123"); // ❌ yield* _が欠落
return user; // Effectが返される、Userではない!
});
// ✅ 正しい:yield* _()でEffectをアンラップ
const program = Effect.gen(function* (_) {
const user = yield* _(fetchUser("123")); // ✅ 正しくアンラップ
return user;
});
エラートラブルシューティング
| エラーメッセージ | 原因 | 解決策 |
|---|---|---|
Missing service: X |
Layerが未提供 | Effect.provide()でLayerを提供 |
Defect: Uncaught error |
Effect内でthrowを使用 | Effect.try()またはEffect.fail()に変更 |
Timeout after X ms |
Effect実行がタイムアウト | 無限ループを確認、timeoutを増やすかリトライを追加 |
Type error: Property '_tag' is missing |
エラー型に_tagフィールドがない | エラークラスにreadonly _tagプロパティを追加 |
Non-exhaustive match |
Matchがすべてのケースをカバーしていない | Match.exhaustiveを追加して完全性を保証 |
Fiber interrupted |
Fiberがキャンセルされた | タイムアウトや手動中断がないか確認 |
Maximum call stack exceeded |
無限再帰 | 再帰Effectに終了条件があるか確認 |
Cannot read property of undefined |
Effectが未実行 | Effect.runPromise()が呼ばれていることを確認 |
Schedule limit exceeded |
リトライ回数が枯渇 | リトライ戦略を確認、リトライ回数を増やすか根本原因を修正 |
Layer cycle detected |
Layerの循環依存 | Layer依存関係をリファクタリングして循環を解消 |
高度な最適化
-
Schemaバリデーション統合:
@effect/schemaを使用してEffectパイプラインの入口で入力データを自動検証。検証失敗時に型安全なValidationErrorを自動生成し、手動バリデーションコードを80%削減 -
分散トレーシング統合:カスタムLayerでOpenTelemetryを統合。各Effectが自動的にtraceIdとspanIdを持ち、エンドツーエンドの分散トレーシングを実現。トラブルシューティング時間を時間単位から分単位に短縮
-
エラー集約と重複排除:エラー集約ミドルウェアを開発し、同じエラーを時間ウィンドウ内で1回のみ報告。アラートノイズを90%削減しつつ、エラー頻度統計を保持
-
適応型リトライ戦略:履歴エラー率とレスポンスタイムに基づく適応型リトリアルゴリズム。バックオフパラメータとリトライ回数を自動調整。エラー率急増時にリトライ頻度を積極的に下げて下流を保護
-
エラーバジェット管理:SLOエラーバジェットシステムを実装。エラーバジェット枯渇時に自動的にトラフィック切り替えやデグレードをトリガーし、コアビジネスの可用性を保護
ソリューション比較
| 項目 | Effect-TS | try-catch | neverthrow | ts-results |
|---|---|---|---|---|
| 型安全性 | ✅ 完全エンコード | ❌ ランタイム | ✅ Result型 | ✅ Result型 |
| エラー合成 | ✅ 自動マージ | ❌ 手動 | ⚠️ 手動必要 | ⚠️ 手動必要 |
| リトライ戦略 | ✅ Schedule | ❌ 手動 | ❌ なし | ❌ なし |
| 依存性注入 | ✅ Layer | ❌ なし | ❌ なし | ❌ なし |
| 並行制御 | ✅ Fiber | ❌ なし | ❌ なし | ❌ なし |
| 学習曲線 | 急 | 緩やか | 中程度 | 中程度 |
| バンドルサイズ | ~50KB | 0 | ~5KB | ~3KB |
| エコシステム統合 | 豊富 | ネイティブ | 限定的 | 限定的 |
| 適したプロジェクト | 大規模アプリ | 小規模スクリプト | 中規模プロジェクト | 中規模プロジェクト |
💡 選択のヒント:プロジェクトが10万行を超え、完全なエラー管理システムが必要な場合、Effect-TSが最適です。シンプルさを重視しプロジェクトが小規模ならneverthrowで十分。単純なスクリプトならtry-catchで問題ありません。
まとめ
エラーは例外ではなく、計算の一部です。Effectシステムはこの哲学を型システムの隅々にまで貫徹しています——エラー型は関数シグネチャにエンコードされ、コンパイラがすべての可能な失敗の処理を強制し、宣言的なリトライとフォールバックが回復ロジックを強力かつ読みやすくします。この5つのコアパターン——Effect基礎と型安全エラー、タグ付きエラーと分類、エラー回復とリトライ、エラー合成とLayer依存、本番級エラー管理——をマスターすれば、「壊れない」TypeScriptアプリケーションを構築するための完全な方法論を手に入れることができます。
オンラインツール推奨
- JSONフォーマッター - Effectエラーレポートとログ出力のフォーマット
- cURL to Code - APIデバッグcURLをEffect-TSサービスコードに変換
- ハッシュ計算 - エラーレポート検証ハッシュを計算し、ログの完全性を確保
ブラウザローカルツールを無料で試す →