Error handling(错误处理)
错误处理用于在程序无法正常完成操作时中断当前流程、传递失败原因,并在合适的边界恢复或释放资源。Dart 使用 throw 抛出对象,使用 try、on、catch 和 finally 处理异常。
学习错误处理时,需要重点掌握以下问题:
- 什么时候应该抛出异常?
- 如何只捕获自己能够处理的异常?
finally和rethrow分别解决什么问题?- 为什么异步异常必须在正确的
await边界处理? - 断言为什么不能代替运行时参数校验?
理解 Exception 和 Error
Dart 的异常都是未检查异常:函数签名不要求声明可能抛出什么,调用方也不会被编译器强制捕获。标准库通常用两类对象表达失败:
| 类型 | 常见含义 | 通常如何处理 |
|---|---|---|
Exception | 操作可能失败,调用方有机会恢复 | 在能够恢复或转换错误的边界捕获 |
Error | 程序违反约束或存在编程错误 | 修复代码,通常不应当作正常业务分支吞掉 |
例如,解析外部文本可能产生 FormatException;给方法传入非法范围可能产生 RangeError。这是一种使用约定,而不是两套不同的捕获机制:两者最终都是对象,都可以被 Dart 的异常机制抛出和捕获。
不要为了“防止崩溃”而无差别捕获所有错误后继续运行。只有当前代码知道如何恢复、补充上下文或转换为更合适的失败结果时,捕获才有意义。
使用 throw 抛出异常
当函数无法履行约定时,可以使用 throw 中断执行:
double percentage(int completed, int total) {
if (total <= 0) {
throw ArgumentError.value(total, 'total', 'must be greater than zero');
}
return completed / total * 100;
}
void main() {
print(percentage(3, 5)); // 60.0
}
优先抛出能够准确表达原因的标准异常,例如 ArgumentError、StateError、FormatException 或 UnsupportedError。业务领域有稳定、独立的失败类型时,也可以定义自己的异常:
class InsufficientBalanceException implements Exception {
InsufficientBalanceException(this.requested, this.available);
final int requested;
final int available;
String toString() =>
'InsufficientBalanceException: requested $requested, available $available';
}
void withdraw(int amount, int balance) {
if (amount > balance) {
throw InsufficientBalanceException(amount, balance);
}
}
throw 是表达式,因此也可以出现在箭头函数或其他需要表达式的位置:
Never unsupportedFormat(String format) =>
throw UnsupportedError('Unsupported format: $format');
这个函数的返回类型是 Never,表示它不会正常返回。
使用 try-catch 处理异常
把可能抛出异常的代码放进 try,并在 catch 中处理失败:
int parsePort(String input) {
try {
return int.parse(input);
} catch (error) {
print('Invalid port: $error');
return 8080;
}
}
void main() {
print(parsePort('not-a-number')); // 8080
}
catch 变量接收抛出的对象。还可以声明第二个变量取得 StackTrace,也就是异常发生时的调用路径:
void logFailure(String input) {
try {
int.parse(input);
} catch (error, stackTrace) {
print('Parsing failed: $error');
print(stackTrace);
}
}
日志、监控或错误包装通常需要堆栈信息;普通用户提示一般不应该直接暴露完整堆栈。
使用 on 捕获指定类型
知道可能出现的异常类型时,使用 on 缩小捕获范围。需要读取异常对象时,可以组合 on 和 catch:
int parsePositiveInt(String input) {
try {
final value = int.parse(input);
if (value <= 0) {
throw RangeError.value(value, 'input', 'must be positive');
}
return value;
} on FormatException catch (error) {
print('Not an integer: ${error.source}');
return 1;
} on RangeError catch (error) {
print(error.message);
return 1;
}
}
void main() {
print(parsePositiveInt('-2')); // 1
}
只关心类型、不需要异常对象时,可以省略 catch:
int parseCount(String input) {
try {
return int.parse(input);
} on FormatException {
return 0;
}
}
多个捕获子句按书写顺序检查。更具体的类型应放在更宽泛的类型前面,否则宽泛分支会先捕获异常,使后面的具体处理无法执行。
使用 finally 释放资源
无论 try 正常完成还是抛出异常,finally 都会执行。它适合关闭文件、连接或其他必须释放的资源:
void processLines(Iterator<String> lines) {
try {
while (lines.moveNext()) {
print(lines.current);
}
} finally {
print('processing finished');
}
}
void main() {
processLines(['first', 'second'].iterator);
}
如果存在匹配的 catch,会先执行 catch,再执行 finally。即使 try 或 catch 中执行了 return,finally 仍会在函数真正返回前运行。
不要在 finally 中随意 return 或抛出新的异常,因为这可能覆盖原本的返回值或失败原因。清理逻辑应该尽量简单,并保留最初异常的诊断信息。
使用 rethrow 保留原始异常
有时当前层只负责记录或补充现场,真正的恢复策略由更外层决定。这时可以在 catch 中使用 rethrow 继续传播当前异常:
int readRequiredNumber(String input) {
try {
return int.parse(input);
} on FormatException catch (error, stackTrace) {
print('Failed to parse "$input": $error');
print(stackTrace);
rethrow;
}
}
void main() {
try {
readRequiredNumber('Dart');
} on FormatException {
print('Please provide a valid number.');
}
}
rethrow 只能在 catch 块中使用,它会保留当前异常及其原始堆栈关系。不要用 throw error 代替单纯的继续传播;如果没有真正处理失败,使用 rethrow 更准确。
在正确位置处理异步异常
返回 Future 的操作可能在稍后才失败。要让 try-catch 捕获这种失败,需要在 try 中 await 对应的 Future:
Future<String> fetchName() async {
await Future<void>.delayed(const Duration(milliseconds: 10));
throw StateError('name is unavailable');
}
Future<void> main() async {
try {
final name = await fetchName();
print(name);
} on StateError catch (error) {
print('Could not load name: ${error.message}');
}
}
如果只在 try 中创建并返回一个尚未等待的 Future,异步失败可能发生在代码离开 try 之后,当前 catch 就无法处理它。可以使用 await 把异常纳入当前控制流,或者使用 Future 的错误处理 API 在明确的异步边界处理。
使用断言检查开发期假设
assert 用于检查开发期间应当始终成立的条件。条件为 false 且断言已启用时,会抛出 AssertionError:
double calculateDiscount(double rate) {
assert(rate >= 0 && rate <= 1, 'rate must be between 0 and 1');
return rate * 100;
}
void main() {
print(calculateDiscount(0.2)); // 20.0
}
断言是否启用取决于运行方式;生产构建通常会禁用断言。因此,assert 不能承担外部输入校验、权限检查、余额检查等生产环境必须执行的逻辑。必须始终验证的条件应使用普通分支,并抛出合适的异常或返回明确的失败结果。
断言表达式也不应包含赋值、写文件等副作用。断言关闭后,这些操作不会执行,程序行为就会发生变化。
不要捕获后静默忽略
下面的代码会隐藏真正的失败原因:
void unsafeParse(String input) {
try {
int.parse(input);
} catch (_) {
// 错误被完全忽略,调用方也不知道操作失败。
}
}
捕获异常后,至少应该完成一种明确动作:恢复为合理默认值、转换为业务失败结果、记录必要上下文,或者使用 rethrow 继续传播。无法处理时,让异常自然向上传递通常比静默忽略更可靠。
TypeScript 对比
Dart 和 TypeScript 都使用 throw、try、catch 和 finally,但类型处理方式不同:
| 对比项 | Dart | TypeScript |
|---|---|---|
| 按类型捕获 | 支持 on FormatException | catch 语法不能直接按类型筛选 |
| 捕获变量 | 可配合 on 得到具体类型 | 开启严格设置时通常按 unknown 缩小 |
| 继续抛出 | rethrow 保留当前异常 | 通常使用 throw error |
| 检查型异常 | 没有 | 没有 |
function parsePort(input: string): number {
try {
const value = Number.parseInt(input, 10);
if (Number.isNaN(value)) {
throw new Error('Invalid port');
}
return value;
} catch (error: unknown) {
if (error instanceof Error) {
console.error(error.message);
}
return 8080;
}
}
两种语言都不强制调用方声明或捕获异常。最需要记忆的是:Dart 可以用 on 直接筛选异常类型,并用 rethrow 继续传播当前异常;异步场景下,两种语言都需要在正确的 await 边界捕获失败。
选择错误处理方式的简单规则
| 需求 | 推荐写法 |
|---|---|
| 当前操作无法履行函数约定 | throw 合适的异常对象 |
| 只处理一种已知异常 | on,按需组合 catch |
| 需要异常对象和堆栈 | catch (error, stackTrace) |
| 无论成功失败都要清理资源 | finally |
| 记录后交给外层继续处理 | rethrow |
捕获 Future 的异步失败 | 在 try 中 await |
| 检查仅在开发期成立的内部假设 | assert |
异常应该用于无法正常完成操作的情况,而不是代替普通分支。尽量在最了解恢复策略的位置捕获具体异常,保留原始原因和堆栈,并确保资源在成功与失败路径上都能正确释放。