name: effect-patterns-error-handling description: Effect-TS 错误处理模式。在Effect-TS应用中处理错误时使用。
Effect-TS 模式:错误处理
此技能提供了3个精选的Effect-TS错误处理模式。 在处理以下任务相关时使用此技能:
- 错误处理
- Effect-TS应用中的最佳实践
- 现实世界的模式和解决方案
🟡 中级模式
错误处理模式1:积累多个错误
规则: 使用错误积累来一次性报告所有问题,而不是早期失败,对于验证和批处理操作至关重要。
好例子:
此示例演示了错误积累模式。
import { Effect, Data, Cause } from "effect";
interface ValidationError {
field: string;
message: string;
value?: unknown;
}
interface ProcessingResult<T> {
successes: T[];
errors: ValidationError[];
}
// 示例1:表单验证与错误积累
const program = Effect.gen(function* () {
console.log(`
[错误积累] 收集多个错误
`);
// 表单数据
interface FormData {
name: string;
email: string;
age: number;
phone: string;
}
const validateForm = (data: FormData): ValidationError[] => {
const errors: ValidationError[] = [];
// 验证1:姓名
if (!data.name || data.name.trim().length === 0) {
errors.push({
field: "name",
message: "姓名必填",
value: data.name,
});
} else if (data.name.length < 2) {
errors.push({
field: "name",
message: "姓名至少2个字符",
value: data.name,
});
}
// 验证2:邮箱
if (!data.email) {
errors.push({
field: "email",
message: "邮箱必填",
value: data.email,
});
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
errors.push({
field: "email",
message: "邮箱格式无效",
value: data.email,
});
}
// 验证3:年龄
if (data.age < 0 || data.age > 150) {
errors.push({
field: "age",
message: "年龄必须在0到150之间",
value: data.age,
});
}
// 验证4:电话
if (data.phone && !/^\d{3}-\d{3}-\d{4}$/.test(data.phone)) {
errors.push({
field: "phone",
message: "电话格式必须为XXX-XXX-XXXX",
value: data.phone,
});
}
return errors;
};
// 示例1:表单有多个错误
console.log(`[1] 表单验证有多个错误:
`);
const invalidForm: FormData = {
name: "",
email: "not-an-email",
age: 200,
phone: "invalid",
};
const validationErrors = validateForm(invalidForm);
yield* Effect.log(`[验证] 找到${validationErrors.length}个错误:
`);
for (const error of validationErrors) {
yield* Effect.log(` ✗ ${error.field}: ${error.message}`);
}
// 示例2:批处理与部分成功
console.log(`
[2] 批处理(积累成功和失败):
`);
interface Record {
id: string;
data: string;
}
const processRecord = (record: Record): Result<string> => {
if (record.id.length === 0) {
return { success: false, error: "缺少ID" };
}
if (record.data.includes("ERROR")) {
return { success: false, error: "无效数据" };
}
return { success: true, value: `processed-${record.id}` };
};
interface Result<T> {
success: boolean;
value?: T;
error?: string;
}
const records: Record[] = [
{ id: "rec1", data: "ok" },
{ id: "", data: "ok" }, // 错误:缺少ID
{ id: "rec3", data: "ok" },
{ id: "rec4", data: "ERROR" }, // 错误:无效数据
{ id: "rec5", data: "ok" },
];
const results: ProcessingResult<string> = {
successes: [],
errors: [],
};
for (const record of records) {
const result = processRecord(record);
if (result.success) {
results.successes.push(result.value!);
} else {
results.errors.push({
field: record.id || "unknown",
message: result.error!,
});
}
}
yield* Effect.log(
`[批处理] 处理了${records.length}条记录`
);
yield* Effect.log(`[批处理] ✓ ${results.successes.length} 成功`);
yield* Effect.log(`[批处理] ✗ ${results.errors.length} 失败
`);
for (const success of results.successes) {
yield* Effect.log(` ✓ ${success}`);
}
for (const error of results.errors) {
yield* Effect.log(` ✗ [${error.field}] ${error.message}`);
}
// 示例3:多步骤验证与错误积累
console.log(`
[3] 多步骤验证(所有检查运行):
`);
interface ServiceHealth {
diskSpace: boolean;
memory: boolean;
network: boolean;
database: boolean;
}
const diagnostics: ValidationError[] = [];
// 检查1:磁盘空间
const diskFree = 50; // MB
if (diskFree < 100) {
diagnostics.push({
field: "disk-space",
message: `只有${diskFree}MB可用(需要100MB)`,
value: diskFree,
});
}
// 检查2:内存
const memUsage = 95; // 百分比
if (memUsage > 85) {
diagnostics.push({
field: "memory",
message: `使用${memUsage}%(阈值:85%)`,
value: memUsage,
});
}
// 检查3:网络
const latency = 500; // 毫秒
if (latency > 200) {
diagnostics.push({
field: "network",
message: `延迟${latency}ms(阈值:200ms)`,
value: latency,
});
}
// 检查4:数据库
const dbConnections = 95;
const dbMax = 100;
if (dbConnections > dbMax * 0.8) {
diagnostics.push({
field: "database",
message: `${dbConnections}/${dbMax}个连接(80%阈值)`,
value: dbConnections,
});
}
if (diagnostics.length === 0) {
yield* Effect.log(`[健康] ✓ 所有系统正常
`);
} else {
yield* Effect.log(
`[健康] ✗ 检测到${diagnostics.length}个问题:
`
);
for (const diag of diagnostics) {
yield* Effect.log(` ⚠ ${diag.field}: ${diag.message}`);
}
}
// 示例4:错误收集与重试决策
console.log(`
[4] 错误收集用于重试策略:
`);
interface ErrorWithContext {
operation: string;
error: string;
retryable: boolean;
timestamp: Date;
}
const operationErrors: ErrorWithContext[] = [];
const operations = [
{ name: "fetch-config", fail: false },
{ name: "connect-db", fail: true },
{ name: "load-cache", fail: true },
{ name: "start-server", fail: false },
];
for (const op of operations) {
if (op.fail) {
operationErrors.push({
operation: op.name,
error: "操作失败",
retryable: op.name !== "fetch-config",
timestamp: new Date(),
});
}
}
yield* Effect.log(`[操作] ${operationErrors.length}个错误:
`);
for (const err of operationErrors) {
const status = err.retryable ? "🔄 可重试" : "❌ 不可重试";
yield* Effect.log(` ${status}: ${err.operation}`);
}
if (operationErrors.every((e) => e.retryable)) {
yield* Effect.log(`
[决策] 所有错误可重试,将重试
`);
} else {
yield* Effect.log(`
[决策] 有些不可重试错误,需要手动干预
`);
}
});
Effect.runPromise(program);
原理:
错误积累策略:
- 收集错误: 在报告前收集所有失败
- 延迟失败: 尽管有错误,继续处理
- 上下文错误: 保持错误位置/操作信息
- 错误摘要: 汇总报告
- 部分成功: 返回有效结果+错误
模式:使用Cause聚合、Result类型或自定义错误结构
快速失败导致问题:
问题1:表单验证
- 用户提交有10个字段错误的表单
- 第一个错误失败:“姓名必填”
- 用户修复姓名,再次提交
- 新错误:“邮箱无效”
- 用户提交10次修复所有错误
- 沮丧,生产力降低
问题2:批处理
- 处理1000条记录,第5条记录失败
- 995条记录未处理
- 用户手动重试
- 为每种错误类型重复
- 低效
问题3:系统诊断
- 服务健康检查失败
- 报告:“检查1失败”
- 修复检查1,服务仍宕机
- 隐藏问题:检查2、3、4也失败
- 浪费时间诊断
解决方案:
错误积累:
- 运行所有验证
- 收集错误
- 报告所有问题
- 用户一次性修复,非10次
部分成功:
- 处理所有记录
- 跟踪成功和失败
- 返回:“950成功,50失败”
- 无需重新处理
全面诊断:
- 运行所有检查
- 报告所有失败
- 快速根本原因分析
- 更快解决
🟠 高级模式
错误处理模式2:错误传播和链
规则: 使用错误传播在效果链中保留上下文,以便在正确的抽象级别进行调试和恢复。
好例子:
此示例演示了带有上下文的错误传播。
import { Effect, Data, Cause } from "effect";
// 领域特定错误与上下文
class DatabaseError extends Data.TaggedError("DatabaseError")<{
query: string;
parameters: unknown[];
cause: Error;
}> {}
class NetworkError extends Data.TaggedError("NetworkError")<{
endpoint: string;
method: string;
statusCode?: number;
cause: Error;
}> {}
class ValidationError extends Data.TaggedError("ValidationError")<{
field: string;
value: unknown;
reason: string;
}> {}
class BusinessLogicError extends Data.TaggedError("BusinessLogicError")<{
operation: string;
context: Record<string, unknown>;
originalError: Error;
}> {}
const program = Effect.gen(function* () {
console.log(`
[错误传播] 带有上下文的错误链
`);
// 示例1:简单错误传播
console.log(`[1] 通过层的错误传播:
`);
const lowLevelOperation = Effect.gen(function* () {
yield* Effect.log(`[层1] 低级操作开始`);
yield* Effect.fail(new Error("文件未找到"));
});
const midLevelOperation = lowLevelOperation.pipe(
Effect.mapError((error) =>
new DatabaseError({
query: "SELECT * FROM users",
parameters: ["id=123"],
cause: error instanceof Error ? error : new Error(String(error)),
})
)
);
const highLevelOperation = midLevelOperation.pipe(
Effect.catchTag("DatabaseError", (dbError) =>
Effect.gen(function* () {
yield* Effect.log(`[层3] 捕获数据库错误`);
yield* Effect.log(`[层3] 查询: ${dbError.query}`);
yield* Effect.log(`[层3] 原因: ${dbError.cause.message}`);
// 恢复决策
return "fallback-value";
})
)
);
const result1 = yield* highLevelOperation;
yield* Effect.log(`[结果] 恢复为: ${result1}
`);
// 示例2:错误上下文积累
console.log(`[2] 通过层积累上下文:
`);
interface ErrorContext {
timestamp: Date;
operation: string;
userId?: string;
requestId: string;
}
const errorWithContext = (context: ErrorContext) =>
Effect.fail(
new BusinessLogicError({
operation: context.operation,
context: {
userId: context.userId,
timestamp: context.timestamp.toISOString(),
requestId: context.requestId,
},
originalError: new Error("操作失败"),
})
);
const myContext: ErrorContext = {
timestamp: new Date(),
operation: "process-payment",
userId: "user-123",
requestId: "req-abc-def",
};
const withContextRecovery = errorWithContext(myContext).pipe(
Effect.mapError((error) => {
// 记录完整上下文
return {
...error,
enriched: true,
additionalInfo: {
serviceName: "payment-service",
environment: "production",
version: "1.2.3",
},
};
}),
Effect.catchAll((error) =>
Effect.gen(function* () {
yield* Effect.log(`[错误捕获] ${error.operation}`);
yield* Effect.log(`[上下文] ${JSON.stringify(error.context, null, 2)}`);
return "recovered";
})
)
);
yield* withContextRecovery;
// 示例3:网络错误与重试上下文
console.log(`
[3] 网络错误与重试上下文:
`);
interface RetryContext {
attempt: number;
maxAttempts: number;
delay: number;
}
let attemptCount = 0;
const networkCall = Effect.gen(function* () {
attemptCount++;
yield* Effect.log(`[尝试] ${attemptCount}/3`);
if (attemptCount < 3) {
yield* Effect.fail(
new NetworkError({
endpoint: "https://api.example.com/data",
method: "GET",
statusCode: 503,
cause: new Error("服务不可用"),
})
);
}
return "success";
});
const withRetryContext = Effect.gen(function* () {
let lastError: NetworkError | null = null;
for (let i = 1; i <= 3; i++) {
const result = yield* networkCall.pipe(
Effect.catchTag("NetworkError", (error) => {
lastError = error;
yield* Effect.log(
`[重试] 尝试${i}失败: ${error.statusCode}`
);
if (i < 3) {
yield* Effect.log(`[重试] 重试前等待...`);
}
return Effect.fail(error);
})
).pipe(
Effect.tap(() => Effect.log(`[成功] 第${i}次尝试连接成功`))
).pipe(
Effect.catchAll(() => Effect.succeed(null))
);
if (result !== null) {
return result;
}
}
if (lastError) {
yield* Effect.fail(lastError);
}
return null;
});
const networkResult = yield* withRetryContext.pipe(
Effect.catchAll((error) =>
Effect.gen(function* () {
yield* Effect.log(`[耗尽] 所有重试失败`);
return "fallback";
})
)
);
yield* Effect.log(`
`);
// 示例4:多层错误转换
console.log(`[4] 层间错误转换:
`);
const layer1Error = Effect.gen(function* () {
yield* Effect.fail(new Error("原始系统错误"));
});
// 层2:转换为领域错误
const layer2 = layer1Error.pipe(
Effect.mapError((error) =>
new DatabaseError({
query: "SELECT ...",
parameters: [],
cause: error instanceof Error ? error : new Error(String(error)),
})
)
);
// 层3:转换为业务错误
const layer3 = layer2.pipe(
Effect.mapError((dbError) =>
new BusinessLogicError({
operation: "fetch-user-profile",
context: {
dbError: dbError.query,
},
originalError: dbError.cause,
})
)
);
// 层4:返回用户友好错误
const userFacingError = layer3.pipe(
Effect.mapError((bizError) => ({
message: "无法加载个人资料",
code: "PROFILE_LOAD_FAILED",
originalError: bizError.originalError.message,
})),
Effect.catchAll((userError) =>
Effect.gen(function* () {
yield* Effect.log(`[用户消息] ${userError.message}`);
yield* Effect.log(`[代码] ${userError.code}`);
yield* Effect.log(`[调试] ${userError.originalError}`);
return null;
})
)
);
yield* userFacingError;
// 示例5:并发操作中的错误聚合
console.log(`
[5] 并发操作中的错误传播:
`);
const operation = (id: number, shouldFail: boolean) =>
Effect.gen(function* () {
if (shouldFail) {
yield* Effect.fail(
new Error(`操作${id}失败`)
);
}
return `result-${id}`;
});
const concurrent = Effect.gen(function* () {
const results = yield* Effect.all(
[
operation(1, false),
operation(2, true),
operation(3, false),
],
{ concurrency: 3 }
).pipe(
Effect.catchAll((errors) =>
Effect.gen(function* () {
yield* Effect.log(`[并发] 捕获聚合错误`);
// 实际代码中,Cause提供错误详情
yield* Effect.log(`[错误] 并发执行期间遇到错误`);
return [];
})
)
);
return results;
});
yield* concurrent;
yield* Effect.log(`
[演示] 错误传播完成`);
});
Effect.runPromise(program);
原理:
错误传播保留上下文:
- 原因链: 保持原始错误+上下文
- 堆栈跟踪: 保留执行历史
- 错误上下文: 添加操作名称、参数
- 错误映射: 在层间转换错误
- 恢复点: 决定在哪里处理错误
模式:使用mapError()、tapError()、catchAll()、Cause.prettyPrint()
错误上下文丢失导致问题:
问题1:无用的错误消息
- 用户看到:“错误: null”
- 调试:从哪里来?何时?为什么?
- 浪费数小时搜索日志
问题2:错误的恢复层
- 网络错误 → 在业务逻辑层恢复(低效)
- 应在网络层恢复 → 重试、指数退避
问题3:错误上下文丢失
- 数据库连接失败
- 但哪个数据库?哪个查询?什么参数?
- 日志显示“连接失败”(不可操作)
问题4:隐藏根本原因
- 效果1失败 → 触发效果2 → 不同错误
- 开发者看到效果2错误
- 不知道效果1是根本原因
- 修复错误的东西
解决方案:
错误上下文:
- 包括操作名称
- 包括相关参数
- 包括时间戳
- 包括重试计数
错误原因链:
- 保持原始错误
- 在每个层添加上下文
mapError()来转换tapError()来记录上下文
恢复层:
- 低级:重试网络请求
- 中级:转换领域错误
- 高级:转换为用户友好消息
错误处理模式3:自定义错误策略
规则: 使用标记错误和自定义错误类型,以实现类型安全的错误处理和业务逻辑感知的恢复策略。
好例子:
此示例演示了自定义错误策略。
import { Effect, Data, Schedule } from "effect";
// 自定义领域错误
class NetworkError extends Data.TaggedError("NetworkError")<{
endpoint: string;
statusCode?: number;
retryable: boolean;
}> {}
class ValidationError extends Data.TaggedError("ValidationError")<{
field: string;
reason: string;
}> {}
class AuthenticationError extends Data.TaggedError("AuthenticationError")<{
reason: "invalid-token" | "expired-token" | "missing-token";
}> {}
class PermissionError extends Data.TaggedError("PermissionError")<{
resource: string;
action: string;
}> {}
class RateLimitError extends Data.TaggedError("RateLimitError")<{
retryAfter: number; // 毫秒
}> {}
class NotFoundError extends Data.TaggedError("NotFoundError")<{
resource: string;
id: string;
}> {}
// 恢复策略选择器
const selectRecoveryStrategy = (
error: Error
): "retry" | "fallback" | "fail" | "user-message" => {
if (error instanceof NetworkError && error.retryable) {
return "retry";
}
if (error instanceof RateLimitError) {
return "retry"; // 带退避
}
if (error instanceof ValidationError) {
return "user-message"; // 用户可修复
}
if (error instanceof NotFoundError) {
return "fallback"; // 使用空结果
}
if (
error instanceof AuthenticationError &&
error.reason === "expired-token"
) {
return "retry"; // 刷新令牌
}
if (error instanceof PermissionError) {
return "fail"; // 不重试
}
return "fail"; // 默认:不重试
};
const program = Effect.gen(function* () {
console.log(
`
[自定义错误策略] 领域感知的错误处理
`
);
// 示例1:类型安全的错误处理
console.log(`[1] 类型安全的错误捕获:
`);
const operation1 = Effect.fail(
new ValidationError({
field: "email",
reason: "无效格式",
})
);
const handled1 = operation1.pipe(
Effect.catchTag("ValidationError", (error) =>
Effect.gen(function* () {
yield* Effect.log(`[捕获] 验证错误`);
yield* Effect.log(` 字段: ${error.field}`);
yield* Effect.log(` 原因: ${error.reason}
`);
return "validation-failed";
})
)
);
yield* handled1;
// 示例2:多种错误类型与不同恢复
console.log(`[2] 每种错误类型的不同恢复:
`);
interface ApiResponse {
status: number;
body?: unknown;
}
const callApi = (shouldFail: "network" | "validation" | "ratelimit" | "success") =>
Effect.gen(function* () {
switch (shouldFail) {
case "network":
yield* Effect.fail(
new NetworkError({
endpoint: "https://api.example.com/data",
statusCode: 503,
retryable: true,
})
);
case "validation":
yield* Effect.fail(
new ValidationError({
field: "id",
reason: "必须为数字",
})
);
case "ratelimit":
yield* Effect.fail(
new RateLimitError({
retryAfter: 5000,
})
);
case "success":
return { status: 200, body: { id: 123 } };
}
});
// 测试每种错误类型
const testCases = ["network", "validation", "ratelimit", "success"] as const;
for (const testCase of testCases) {
const strategy = yield* callApi(testCase).pipe(
Effect.catchTag("NetworkError", (error) =>
Effect.gen(function* () {
yield* Effect.log(
`[网络] 可重试: ${error.retryable}, 状态: ${error.statusCode}`
);
return "will-retry";
})
),
Effect.catchTag("ValidationError", (error) =>
Effect.gen(function* () {
yield* Effect.log(
`[验证] ${error.field}: ${error.reason} (不重试)`
);
return "user-must-fix";
})
),
Effect.catchTag("RateLimitError", (error) =>
Effect.gen(function* () {
yield* Effect.log(
`[速率限制] 重试后 ${error.retryAfter}ms`
);
return "retry-with-backoff";
})
),
Effect.catchAll((error) =>
Effect.gen(function* () {
yield* Effect.log(`[成功] 获取响应`);
return "completed";
})
)
);
yield* Effect.log(` 策略: ${strategy}
`);
}
// 示例3:基于错误的自定义重试策略
console.log(`[3] 错误特定的重试策略:
`);
let attemptCount = 0;
const networkOperation = Effect.gen(function* () {
attemptCount++;
yield* Effect.log(`[尝试] ${attemptCount}`);
if (attemptCount === 1) {
yield* Effect.fail(
new NetworkError({
endpoint: "api.example.com",
statusCode: 502,
retryable: true,
})
);
}
if (attemptCount === 2) {
yield* Effect.fail(
new RateLimitError({
retryAfter: 100,
})
);
}
return "success";
});
// 类型安全的重试与错误分类
let result3: string | null = null;
for (let i = 0; i < 3; i++) {
result3 = yield* networkOperation.pipe(
Effect.catchTag("NetworkError", (error) =>
Effect.gen(function* () {
if (error.retryable && i < 2) {
yield* Effect.log(`[重试] 网络错误可重试`);
return null; // 信号重试
}
yield* Effect.log(`[失败] 网络错误不可重试`);
return Effect.fail(error);
})
),
Effect.catchTag("RateLimitError", (error) =>
Effect.gen(function* () {
yield* Effect.log(
`[退避] 速率限制,等待 ${error.retryAfter}ms`
);
yield* Effect.sleep(`${error.retryAfter} millis`);
return null; // 信号重试
})
),
Effect.catchAll((error) =>
Effect.gen(function* () {
yield* Effect.log(`[错误] 未处理: ${error}`);
return Effect.fail(error);
})
)
).pipe(
Effect.catchAll(() => Effect.succeed(null))
);
if (result3 !== null) {
break;
}
}
yield* Effect.log(`
[结果] ${result3}
`);
// 示例4:错误感知的业务逻辑
console.log(`[4] 错误处理的业务逻辑:
`);
interface User {
id: string;
email: string;
}
const loadUser = (id: string): Effect.Effect<User, NetworkError | NotFoundError> =>
Effect.gen(function* () {
if (id === "invalid") {
yield* Effect.fail(
new NotFoundError({
resource: "user",
id,
})
);
}
if (id === "network-error") {
yield* Effect.fail(
new NetworkError({
endpoint: "/api/users",
retryable: true,
})
);
}
return { id, email: `user-${id}@example.com` };
});
const processUser = (id: string) =>
loadUser(id).pipe(
Effect.catchTag("NotFoundError", (error) =>
Effect.gen(function* () {
yield* Effect.log(
`[业务] 用户未找到: ${error.id}`
);
// 返回默认/空用户
return { id: "", email: "" };
})
),
Effect.catchTag("NetworkError", (error) =>
Effect.gen(function* () {
yield* Effect.log(
`[业务] 网络错误,将从缓存重试`
);
return { id, email: "cached@example.com" };
})
)
);
yield* processUser("valid-id");
yield* processUser("invalid");
yield* processUser("network-error");
// 示例5:判别联合用于完整性检查
console.log(`
[5] 完整性检查(编译时安全):
`);
const classifyError = (
error: NetworkError | ValidationError | AuthenticationError | PermissionError
): string => {
switch (error._tag) {
case "NetworkError":
return `网络: ${error.statusCode}`;
case "ValidationError":
return `验证: ${error.field}`;
case "AuthenticationError":
return `认证: ${error.reason}`;
case "PermissionError":
return `权限: ${error.action}`;
// TypeScript确保所有情况覆盖
default:
const _exhaustive: never = error;
return _exhaustive;
}
};
const testError = new ValidationError({
field: "age",
reason: "必须>=18",
});
const classification = classifyError(testError);
yield* Effect.log(`[分类] ${classification}`);
// 示例6:恢复策略链
console.log(`
[6] 链式恢复策略:
`);
const resilientOperation = Effect.gen(function* () {
yield* Effect.fail(
new RateLimitError({
retryAfter: 50,
})
);
});
const withRecovery = resilientOperation.pipe(
Effect.catchTag("RateLimitError", (error) =>
Effect.gen(function* () {
yield* Effect.log(
`[步骤1] 捕获速率限制,等待 ${error.retryAfter}ms`
);
yield* Effect.sleep(`${error.retryAfter} millis`);
// 再次尝试
return yield* Effect.succeed("recovered");
})
),
Effect.catchTag("NetworkError", (error) =>
Effect.gen(function* () {
if (error.retryable) {
yield* Effect.log(`[步骤2] 网络错误,重试中...`);
return "retry";
}
return yield* Effect.fail(error);
})
),
Effect.catchAll((error) =>
Effect.gen(function* () {
yield* Effect.log(`[步骤3] 最终回退`);
return "fallback";
})
)
);
yield* withRecovery;
});
Effect.runPromise(program);
原理:
自定义错误策略实现业务逻辑:
- 标记错误: 使用Effect.Data实现类型安全的错误
- 错误分类: 可重试、瞬态、永久
- 领域语义: 业务有意义的错误
- 恢复策略: 每种错误类型不同
- 错误上下文: 包括恢复提示
模式:使用Data.TaggedError、错误判别器、catchTag()
通用错误阻止最佳恢复:
问题1:一刀切的重试
- 网络超时(瞬态,带退避重试)
- 无效API密钥(永久,不重试)
- 两者相同处理 = 错误恢复
问题2:丢失业务意图
- 系统错误:“连接被拒绝”
- 业务含义:不明确
- 用户消息:“出现问题”(无帮助)
问题3:错误的恢复层
- 应在网络层重试
- 反而在应用层重试
- 浪费计算,用户体验差
问题4:静默失败
- 多种错误类型可能
- 通用捕获忽略区别
- 错误:将错误A当作错误B处理
- 数据损坏,难以调试
解决方案:
标记错误:
NetworkError、ValidationError、PermissionError- 类型系统确保处理
- TypeScript编译器捕获遗漏情况
- 清晰意图
恢复策略:
NetworkError→ 带指数退避重试ValidationError→ 返回用户消息,不重试PermissionError→ 记录安全事件,不重试TemporaryError→ 带抖动重试
业务语义:
- 错误类型匹配领域概念
- 代码像领域语言一样读
- 更易维护
- 新开发者快速理解