How to Use DartExceptor: A Lighter Way to Handle Errors in Dart 3
TL;DR · AI 摘要
DartExceptor 提供了一种更轻量的错误处理方式,通过类型系统和结构化错误类型提升 Dart 3 的错误处理能力。
核心要点
- 使用 DartExceptor 可以通过类型系统明确错误类型,避免错误被忽略。
- DartExceptor 的 API 设计为四个方法,每个方法只负责一项任务,提升代码可读性。
- DartExceptor 与 Clean Architecture 结合,使错误处理更符合分层架构原则。
结构提纲
按章节快速跳转。
- §引言
传统错误处理方式存在诸多问题,如错误不可见、编译器无法帮助检测错误等。
上一篇文章介绍了使用 Dart Records 和 sealed Result 类型来改进错误处理。
即使使用了 sealed Result 类型,仍然存在错误处理不够明确的问题。
DartExceptor 通过结构化错误类型和 API 设计,使错误处理更清晰、更安全。
DartExceptor 的核心类型 AppResult<T> 提供了 AppSuccess 和 AppFailure 两个子类。
DartExceptor 的 API 设计为四个方法,每个方法只负责一项任务,提升代码可读性。
思维导图
用一张图看清主题之间的关系。
查看大纲文本(无障碍 / 无 JS 友好)
- DartExceptor 错误处理
- 问题
- 错误不可见
- 编译器无法帮助检测错误
- 错误处理不明确
- 解决方案
- 使用 sealed Result 类型
- 结构化错误类型
- API 设计为四个方法
金句 / Highlights
值得收藏与分享的关键句。
Traditional error handling in Dart can lead to invisible failures, compiler helplessness, and indiscriminate error catching.
DartExceptor introduces a sealed Result type, AppResult<T>, which replaces nullable-field records with two structurally distinct subclasses.
DartExceptor's API is designed with four methods, each with one job, making error handling more readable and maintainable.
如何使用 DartExceptor:一种更轻量的方式来处理 Dart 3 中的错误
2026年6月17日
/
#Dart
Oluwaseyi Fatunmole
如果你已经使用 Flutter 有一段时间了,你可能已经写过下面这段代码:
try {
final user = await repo.getUser();
print(user.name);
} catch (e) {
print('Something went wrong: $e');
}这段代码可以编译,可以发布。但六个月后,用户可能会报告一个 bug,他们面对的是一个空白屏幕,因为某个地方的 catch (e) 把真正的错误给吞掉了。
这段代码看起来无害,但它有三个问题,只有在压力之下才会显现出来。
首先,错误在签名中是不可见的。无论 repo.getUser() 返回什么,都无法告诉你网络中断、令牌过期或响应格式错误时会发生什么。你只有通过阅读实现代码,或者在生产环境中遇到 bug 才能发现。
其次,编译器帮不上忙。如果同事在代码库的其他地方忘了添加 try/catch,应用仍然可以正常编译。没有任何警告。崩溃发生在运行时,面对的是真正的用户,而不是你在构建时看到的。
第三,catch (e) 会无差别地捕获一切。一个拼写错误、一个空引用、一个真正的网络错误和一个格式错误的 JSON 响应都会被放到同一个代码块中。你无法在不检查错误字符串的情况下区分它们,而这种方式很脆弱,因为只要消息一改变,就会出问题。
总的来说,每一条错误路径都变成了函数作者和调用者之间的社会契约,而不是类型系统强制执行的规则。社会契约在高压下、在大型团队中、在凌晨两点的事件处理中都会破裂。
几周前,我写了一篇《Dart 中的高级错误处理:记录、结果类型、单子和冻结异常》,详细介绍了如何使用记录、密封结果类型、单子模式、dartz 和冻结异常来解决这个问题,使错误变得类型化、可见且无法忽视。
本文旨在独立存在,因此在我们重新拾起这个话题之前,先快速回顾一下前一篇文章的结论。
我们将涵盖的内容:
- 回顾:前一篇文章的结尾
- 模式之后的问题
- DartExceptor 的工作原理
- 核心类型
- API:四个方法,每个方法只做一件事
- 它在整洁架构中的位置
- 为什么不直接使用 dartz?
- 试用一下
回顾:前一篇文章的结尾
那篇文章通过了多个层次,每一层都解决了上一层的限制。
它从 Dart 记录开始,作为最简单的修复方式,一个带有可空字段的成功和失败的类型化元组:
typedef Result<E, T> = ({E? e, T? data});这已经比裸异常更好,因为返回类型现在允许函数失败。
但记录确实有一个真正的限制。你很容易忘记检查哪个字段被填充了,而且没有方法可以在不手动解包它的情况下转换结果。
这个缺口导致了正确的密封结果类型 AppResult<T>,它用两个结构上不同的子类 AppSuccess 和 AppFailure 替换了可空字段记录,并添加了一个 when() 方法,强制处理这两种情况:
sealed class AppResult<T> {
const AppResult();
R when<R>({
required R Function(T value) success,
required R Function(AppFailure failure) failure,
});
}
class AppSuccess<T> extends AppResult<T> {
const AppSuccess(this.value);
final T value; @override
R when<R>({
required R Function(T value) success,
required R Function(AppFailure failure) failure,
}) => success(value);
}
class AppFailure<T> extends AppResult<T> {
const AppFailure(this.error);
final AppError error;
@override
R when<R>({
required R Function(T value) success,
required R Function(AppFailure failure) failure,
}) => failure(this);
}由于 AppResult 是密封的,编译器强制执行穷尽性检查。你真的无法像使用记录或 try/catch 那样忘记 failure 分支。
从那里开始,文章将 AppResult 扩展为一个真正的 Monad,通过添加 map 和 flatMap,使得结果可以在不离开包装器的情况下进行转换和链式调用,并引入了 dartz 的 Either 作为更传统的函数式编程等价物,供希望使用该术语的团队使用。最后,文章引入了基于 Freezed 的类型化异常,这样即使在失败的一侧,也携带结构化、可模式匹配的数据,而不是一个裸字符串。
最终,这个模式在全栈中看起来是这样的:一个密封的结果类型、结构化的异常,以及用于转换的 map / flatMap,这些在仓库、领域和展示层中保持一致地连接。
如果你想了解完整的推导过程,为什么每个层被添加,dartz 的集成,以及 Freezed 异常的设置,那篇文章都深入地涵盖了这些内容。这里所遵循的只是上述的形状,而不是到达那里的旅程。
模式之后的问题
在我发表那篇文章之后发生了以下事情。
每次我开始一个新项目时,我发现自己总是在做同样的事情:重新创建密封的 Result 类,重新编写 Ok 和 Err,重新实现 map、flatMap 和其余部分。从项目到项目复制大约 150 行的相同代码,对小细节进行调整,偶尔因为忘记上次命名了什么而在项目之间引入不一致。
模式是正确的。重复是不必要的。
一个每次都要重写模式的模式不是一个模式,而是一种负担。所以我将它打包成一个库。
DartExceptor 的工作原理
DartExceptor 是一个轻量级、零依赖的 Dart 3 包,它实现了前一篇文章中提到的精确模式,包括 Trace<T, E>、Ok、Err,以及一组小而有意的 Monad 操作,作为可重用的包。
不需要 dartz、不需要 Freezed,也不需要 build_runner。只有 Trace<T, E>、两个实现和四个方法。
dependencies:
dart_exceptor: ^1.1.2import 'package:dart_exceptor/dart_exceptor.dart';这就是全部的设置。
核心类型
DartExceptor 中的每个操作都返回一个 Trace<T, E>:
- T 是成功类型
- E 是错误类型
Trace 恰好有两个实现:
return Ok(user); // 成功
return Err(AppException(code: 404, e: 'Not found')); // 失败你从不直接构造 Trace。你返回 Ok 或 Err,并在其他所有地方以 Trace 为对象进行编程。函数签名现在如实说明了可能发生的情况:
Future<Trace<User, AppException>> getUser(String id);任何阅读该签名的人都会立即知道这可能会成功返回一个 User,或者失败返回一个 AppException。六个月后不会有意外。
API:四个方法,每个方法只做一件事
如果前一篇文章中的 Result 类型有 map、flatMap 和一个用于模式匹配的 when(),DartExceptor 将采用相同的形状,并将其精炼为四个专注的方法。
split,退出点
split 是你离开 Trace 世界的地方。两个处理器都是必需的,因此你不能不小心忽略失败路径。
result.split(
data: (user) => print(user.name),
e: (e) => print(e.message),
);map,提取并转换成功结果
map 会从 Ok 中提取值并允许你直接对其进行转换:
final activeUsers = result.map(
data: (users) => users.where((u) => u.isActive).toList(),
);mapError,提取并转换失败结果
这是 map 的镜像,用于错误处理。当跨越架构边界时,它非常有用,这时你的数据层的异常类型可能与你的领域层不同:
final domainError = result.mapError(
e: (e) => AppException(code: e.statusCode, e: e.toString()),
);bind<B>,连接返回 Trace 的操作
这是真正执行操作的部分。bind<B> 允许你连接返回 Trace 的操作,每一步都转换成功类型。如果任何一步失败,所有后续操作会自动被跳过。
result
.bind<User>(
n: (users) {
try {
return Ok(users.firstWhere((u) => u.id == id));
} catch (e) {
return Err(AppException(code: 404, e: 'User not found'));
}
},
)
.bind<String>(n: (user) => Ok(user.firstName))
.split(
data: (name) => print('User: $name'),
e: (e) => print('Error: ${e.e}'),
);List<User> 变成 User 变成 String。每个 bind<B> 都会转换类型,编译器会检查每一步,链式中的任何失败都会直接跳转到 split 中的 e 处理器。这是前一篇文章中 flatMap 讨论的逻辑结论。
在 Clean 架构中的位置
原始文章中的模式始终不仅仅是关于语法,而是关于在各层之间使失败可见。DartExceptor 完美地融入了这种结构,无需任何修改:
// 数据层
abstract class DataSource {
Future<Trace<List<User>, AppException>> getAllUsers();
}
// 仓库层
abstract class IUserRepository {
Future<Trace<List<User>, AppException>> getAllUsers();
}
// 用例层
class UserUseCase {
Future<Trace<List<User>, AppException>> getAllUsers() => repository.getAllUsers();
}
// 表示层
void loadUsers() async {
final result = await useCase.getAllUsers();
result.split(
data: (users) => print('Loaded ${users.length} users'),
e: (e) => print('Failed: ${e.e}'),
);
}相同的层,相同的分离,相同类型的失败路径,只是无需每次重新构建基础。
为什么不直接使用 dartz?
前一篇文章详细介绍了 dartz 的 Either,如果你的团队熟悉其 API 表面,并且不介意依赖项的开销,它确实是一个非常可靠的选择。
DartExceptor 的存在是为了更狭窄的情况,即当你想要使用结果类型模式,而无需导入基于 Haskell 风格函数式编程惯例的库。没有 Left / Right,没有 fold,也没有传递依赖。只有 Trace、Ok、Err,以及四个直接映射到前一篇文章中模式实际使用方式的方法。
| DartExceptor | dartz | |--------------|-------| | 依赖项 | 零 | 多个 | | Dart 3 原生支持 | 是 | 否 | | API 表面 | 4 个方法 | 大 | | 需要 Haskell 概念 | 是 | 否 | | 类型安全链式(bind<B>) | 是 | 是(flatMap) |
尝试一下
DartExceptor 已经发布在 pub.dev 上:
包:pub.dev/packages/dart_exceptor 源代码:GitHub
如果你已经阅读了上一篇文章,并且自己实现了一个类似的功能,我真心想听听你的版本与之相比如何。如果 DartExceptor 能够帮你避免再次重写这个模式,那么在 GitHub 上给一个星标将会有很大帮助。
我是拥有 7 年以上经验的移动工程负责人和高级软件工程师,专注于构建和领导可扩展、企业级跨平台移动应用的交付。我在系统设计方面有丰富的经验,涵盖从架构到部署的整个流程,特别关注性能、可扩展性和可维护的代码。我曾领导工程团队,提高交付效率、代码质量和团队协作,同时指导开发人员在技术和职业上不断成长。我的核心技术栈包括 Flutter、Dart、.NET 和 .NET Core,有将移动系统与强大的后端架构集成的经验。除了交付工作,我还开发并发布 Flutter 插件,为开源项目做贡献,并撰写技术内容,帮助简化复杂的工程概念。我处于实际工程与技术领导的交汇点,专注于构建具有影响力的产品,并赋能高效团队。
如果你读到这里,请感谢作者,以表达你对他们的关心。说声谢谢
免费学习编程。freeCodeCamp 的开源课程已帮助超过 40,000 人成为开发人员。立即开始
ADVERTISEMENT