跳到主要内容

Error handling(错误处理)

错误处理用于在程序无法正常完成操作时中断当前流程、传递失败原因,并在合适的边界恢复或释放资源。Dart 使用 throw 抛出对象,使用 tryoncatchfinally 处理异常。

学习错误处理时,需要重点掌握以下问题:

  • 什么时候应该抛出异常?
  • 如何只捕获自己能够处理的异常?
  • finallyrethrow 分别解决什么问题?
  • 为什么异步异常必须在正确的 await 边界处理?
  • 断言为什么不能代替运行时参数校验?

理解 ExceptionError

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
}

优先抛出能够准确表达原因的标准异常,例如 ArgumentErrorStateErrorFormatExceptionUnsupportedError。业务领域有稳定、独立的失败类型时,也可以定义自己的异常:

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 缩小捕获范围。需要读取异常对象时,可以组合 oncatch

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。即使 trycatch 中执行了 returnfinally 仍会在函数真正返回前运行。

不要在 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 捕获这种失败,需要在 tryawait 对应的 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 都使用 throwtrycatchfinally,但类型处理方式不同:

对比项DartTypeScript
按类型捕获支持 on FormatExceptioncatch 语法不能直接按类型筛选
捕获变量可配合 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 的异步失败tryawait
检查仅在开发期成立的内部假设assert

异常应该用于无法正常完成操作的情况,而不是代替普通分支。尽量在最了解恢复策略的位置捕获具体异常,保留原始原因和堆栈,并确保资源在成功与失败路径上都能正确释放。