提问人:leepowell 提问时间:11/6/2023 更新时间:11/16/2023 访问量:95
使用 Eithers 处理调用链传递的错误的方法
Approach to handle errors passed up the call chain using Eithers
问:
我目前正在学习更多关于函数式编程和错误处理的替代方法,而不是我习惯的(主要是尝试/捕获)。并且一直在研究各种编程语言中的 Either monad。我一直在尝试将这个概念应用于一个使用 Express 和 fp-ts 的小型简单应用程序。
假设我有以下用于处理请求的体系结构(假设它是从数据库中检索实体):
Express -> route handler -> controller -> repository -> data source -> database
这为错误提供了多个可能发生的地方。首先,让我们看一下数据源。我正在尝试将其限制为一个接口,以便将来在需要时交换数据源。所以我的数据源界面最初看起来像:
export type TodoDataSource = {
findAll(): Promise<ReadonlyArray<TodoDataSourceDTO>>;
findOne(id: string): Promise<TodoDataSourceDTO | null>;
create(data: CreateTodoDTO): Promise<TodoDataSourceDTO>;
update(id: string, data: UpdateTodoDTO): Promise<TodoDataSourceDTO>;
remove(id: string): Promise<TodoDataSourceDTO>;
};
然后,此数据源在创建时传递到存储库工厂函数:
export function createTodoRepository(
dataSource: TodoDataSource,
): TodoRepository {
return {
findAll: async () => await findAll(dataSource),
findOne: async (id: string) => await findOne(dataSource, id),
create: async (data: CreateTodoDTO) => await create(dataSource, data),
update: async (id: string, data: UpdateTodoDTO) =>
await update(dataSource, id, data),
remove: async (id: string) => await remove(dataSource, id),
};
}
以类似的方式,我的存储库实现旨在满足接口的契约:TodoRepository
export type TodoRepository = {
findAll(): Promise<ReadonlyArray<Todo>>;
findOne(id: string): Promise<Todo | null>;
create(data: CreateTodoDTO): Promise<Todo>;
update(id: string, data: UpdateTodoDTO): Promise<Todo>;
remove(id: string): Promise<Todo>;
};
我遇到的问题是,一旦我尝试应用使用s的方法,我的接口就会开始变得非常紧密地耦合并且非常冗长。Either
首先,让我将数据源更新为用作其返回类型:Either
import * as TaskEither from 'fp-ts/TaskEither';
export type DataSourceError = { type: "DATA_SOURCE_ERROR"; error?: unknown };
export type TodoDataSource = {
findAll(): Promise<
TaskEither.TaskEither<DataSourceError, ReadonlyArray<TodoDataSourceDTO>>
>;
findOne(
id: string,
): Promise<TaskEither.TaskEither<DataSourceError, TodoDataSourceDTO | null>>;
create(
data: CreateTodoDTO,
): Promise<TaskEither.TaskEither<DataSourceError, TodoDataSourceDTO>>;
update(
id: string,
data: UpdateTodoDTO,
): Promise<TaskEither.TaskEither<DataSourceError, TodoDataSourceDTO>>;
remove(
id: string,
): Promise<TaskEither.TaskEither<DataSourceError, TodoDataSourceDTO>>;
};
还不错。这将处理基础数据源抛出的任何意外情况 - 我们可以捕获并转换为 .伟大。Either
现在到我的存储库界面:
import type * as TaskEither from "fp-ts/TaskEither";
import { type DataSourceError } from "../../infra/data-sources/todo.data-source";
import { type ParseError } from "../../shared/parsers";
export type TodoNotFoundError = { type: "TODO_NOT_FOUND"; id: string };
export type TodoRepository = {
findAll(): Promise<
TaskEither.TaskEither<DataSourceError | ParseError, ReadonlyArray<Todo>>
>;
findOne(
id: string,
): Promise<
TaskEither.TaskEither<
DataSourceError | ParseError | TodoNotFoundError,
Todo
>
>;
create(
data: CreateTodoDTO,
): Promise<TaskEither.TaskEither<DataSourceError | ParseError, Todo>>;
update(
id: string,
data: UpdateTodoDTO,
): Promise<TaskEither.TaskEither<DataSourceError | ParseError, Todo>>;
remove(
id: string,
): Promise<TaskEither.TaskEither<DataSourceError | ParseError, Todo>>;
};
现在,我的存储库和数据源开始与它们的错误类型耦合。但是,存储库现在开始在此处添加一些自己的错误类型,例如(当将 DTO 解析到域实体失败时)和(当 findOne 函数返回 null 时 - 尽管使用 Option 可能更好)。PARSE_ERROR
TODO_NOT_FOUND
我想这还不错,但我们只有两个调用深度,我的存储库不应该处理对用户出错的响应,我应该让这些错误将调用链传递回控制器,以便控制器可以使用适当的响应代码/文本进行响应。
在这种情况下,我认为它很容易处理,因为调用堆栈很简单,但是如果控制器最终调用了多个服务和/或存储库,并且我最终不得不键入所有接口及其可能的左错误类型,该怎么办?
我是否错误地处理了这一点?或者这是意料之中的?
另一种方法是混合使用这两种方法,使用 for 异常,并在控制器上捕获它们。并且使用 for 错误,我可以在调用堆栈的早期处理。try/catch
Either
答:
您已经深入研究了函数式编程中错误处理的一个基本方面,尤其是使用 Either monad。虽然类型中的详细程度和增加的耦合最初可能看起来势不可挡,但在 FP 中,在每一层显式处理错误类型是很常见的。
你对在调用堆栈中传播错误的担忧是有道理的。正如您正确提到的,控制器不一定应该处理低级错误细节;它更适合在请求上下文中解释错误并制定适当的响应。
一个潜在的改进可能是引入常见的错误并集类型或更精细的错误处理策略。例如:
type AppError =
| DataSourceError
| ParseError
| TodoNotFoundError
// Add more specific errors as necessary
// In the repository interface
export type TodoRepository = {
findAll(): Promise<TaskEither.TaskEither<AppError, ReadonlyArray<Todo>>>;
findOne(id: string): Promise<TaskEither.TaskEither<AppError, Todo>>;
// ...
}
这种合并可能会减少类型中的一些冗长,并提供一种更集中的方法来处理错误。此外,随着应用程序的增长,考虑显式错误处理与其引入的复杂性之间的权衡至关重要。
关于将 try/catch 与 Either 混合使用,这是一种有效的方法。您可以为真正意外的异常情况(如灾难性故障)保留 try/catch,同时将 Either 用于域中的可恢复错误或预期故障方案。
混合使用这两种方法可以在 FP 的显式错误处理和传统的基于异常的处理之间提供平衡,但在两者之间保持清晰的边界对于代码库的清晰度和一致性至关重要。
请记住,在错误处理的显性和代码维护的实用性之间找到适当的平衡是关键。每个项目可能有不同的要求和权衡,因此相应地调整错误处理策略是一种很好的做法。
评论
Error
Task
Promise
Promise
Promise<Task<...>>
Promise<Promise<...>>
TaskEither
() => Promise<Either<A, B>>
TaskEither
Task
Promise