Dart: Dart 异常处理 — 编写健壮的错误处理代码
异常处理是代码的安全网 — 没有它,一个错误就能让整个系统崩溃。
1. 你将学到
- Exception vs Error 的语义区别
- try / on / catch / finally 完整语法
- 自定义异常类与错误码设计
- rethrow 与异常链
- Bob 场景:DataPipeline 文件解析异常与网络超时处理
2. 一个开发者的真实故事
(1) 痛点:未处理的异常让批处理任务中途崩溃
Bob 的 DataPipeline 在处理百万级订单时,因为一个格式错误的 CSV 行导致 FormatException,整个处理任务崩溃。800,000 条已处理的数据全部丢失,需要从头重跑。更糟的是,错误信息只显示 "FormatException",没有行号和上下文,Bob 花了 4 小时才定位到问题。
(2) 异常处理的解法
用 try-on-catch-finally 捕获特定异常,自定义异常类携带上下文信息,finally 确保资源释放。
try {
final records = await parseCsvFile(path);
await processRecords(records);
} on FormatException catch (e) {
log.error('CSV parse error: ${e.message} at line ${e.offset}');
// Skip malformed records, continue processing
} on TimeoutException {
log.error('API timeout, retrying...');
await retryWithBackoff();
} finally {
await closeResources();
}
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
(3) 收益
- 单条记录错误不再导致整批任务失败
- 自定义异常携带行号、字段等上下文,定位时间从 4 小时降到 5 分钟
- finally 确保文件句柄和网络连接始终被释放
3. Exception vs Error
(1) 语义区别
flowchart TD A[Throwable] --> B[Error<br/>Recoverable: NO] A --> C[Exception<br/>Recoverable: YES] B --> B1[OutOfMemoryError] B --> B2[StackOverflowError] C --> C1[FormatException] C --> C2[TimeoutException] C --> C3[IOException] C --> C4[Custom Exception]
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
| 维度 | Error | Exception |
|---|---|---|
| 可恢复性 | 不可恢复 | 可恢复 |
| 应捕获否 | 不应该 | 应该 |
| 产生者 | VM / 运行时 | 应用代码 |
| 示例 | StackOverflowError | FormatException |
4. try / on / catch / finally
(1) 完整语法
▶ 示例
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
:基本 try-catch
void main() {
try {
final result = int.parse('abc');
print(result);
} on FormatException catch (e) {
print('Format error: ${e.message}');
} catch (e) {
print('Unexpected error: $e');
}
}
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
▶ 示例
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
:on 与 catch 的区别
void main() {
// on - catches specific type, no access to exception object
try {
int.parse('not a number');
} on FormatException {
print('Caught FormatException (no details needed)');
}
// on + catch - catches specific type WITH access to exception
try {
int.parse('not a number');
} on FormatException catch (e) {
print('Caught: ${e.message}');
}
// catch with stack trace
try {
int.parse('not a number');
} on FormatException catch (e, stackTrace) {
print('Error: $e');
print('Stack: $stackTrace');
}
}
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
▶ 示例
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
:finally 块
import 'dart:io';
void main() async {
File? file;
try {
file = File('orders.csv');
final content = await file.readAsString();
print('Read ${content.length} characters');
} on FileSystemException catch (e) {
print('File error: ${e.message}');
} finally {
// Always executed - even if return or throw
print('Cleanup: file handle released');
}
}
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
| 子句 | 作用 | 可有多个 | 顺序 |
|---|---|---|---|
try |
包裹可能抛异常的代码 | 1 | 第1 |
on Type |
捕获特定类型 | 多个 | try后 |
catch (e) |
捕获任意异常 | 1 | on后 |
finally |
始终执行 | 1 | 最后 |
5. 自定义异常类
(1) 异常类设计原则
▶ 示例
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
:自定义异常类
// Base exception for DataPipeline
class PipelineException implements Exception {
final String message;
final String? source;
final int? lineNumber;
PipelineException(this.message, {this.source, this.lineNumber});
@override
String toString() => 'PipelineException: $message'
'${source != null ? " (source: $source)" : ""}'
'${lineNumber != null ? " at line $lineNumber" : ""}';
}
// Specific exception types
class DataFormatException extends PipelineException {
final String fieldName;
final String invalidValue;
DataFormatException({
required this.fieldName,
required this.invalidValue,
required super.message,
super.source,
super.lineNumber,
});
@override
String toString() => 'DataFormatException: $message '
'(field: $fieldName, value: "$invalidValue")';
}
class NetworkTimeoutException extends PipelineException {
final Duration timeout;
final String endpoint;
NetworkTimeoutException({
required this.timeout,
required this.endpoint,
super.message = 'Request timed out',
}) : super(message);
@override
String toString() => 'NetworkTimeout: ${timeout.inSeconds}s '
'timeout on $endpoint';
}
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
▶ 示例
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
:错误码设计
enum ErrorCode {
fileNotFound('E001', 'File not found'),
invalidFormat('E002', 'Invalid data format'),
networkTimeout('E003', 'Network request timed out'),
authFailed('E004', 'Authentication failed'),
rateLimitExceeded('E005', 'Rate limit exceeded');
final String code;
final String description;
const ErrorCode(this.code, this.description);
}
class CodedException extends PipelineException {
final ErrorCode errorCode;
CodedException(this.errorCode, {String? detail})
: super('${errorCode.code}: ${errorCode.description}'
'${detail != null ? " - $detail" : ""}');
@override
String toString() => '[$errorCode] $message';
}
void main() {
try {
throw CodedException(ErrorCode.invalidFormat, detail: 'amount field is not a number');
} on CodedException catch (e) {
print(e); // [ErrorCode.invalidFormat] E002: Invalid data format - amount field is not a number
}
}
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
6. rethrow 与异常链
▶ 示例
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
:rethrow
double parseAmount(String input) {
try {
return double.parse(input);
} on FormatException catch (e) {
// Log and rethrow - don't swallow the exception
print('Failed to parse amount: "$input"');
rethrow; // Preserves original stack trace
}
}
void main() {
try {
final amount = parseAmount('not_a_number');
} on FormatException {
print('Caught rethrown exception');
}
}
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
▶ 示例
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
:异常链
class ChainedException implements Exception {
final String message;
final Exception? innerException;
ChainedException(this.message, {this.innerException});
@override
String toString() {
var result = 'ChainedException: $message';
if (innerException != null) {
result += '\n Caused by: $innerException';
}
return result;
}
}
Future``<double>`` fetchOrderAmount(String orderId) async {
try {
// Simulate API call
throw FormatException('Invalid JSON response');
} on FormatException catch (e) {
throw ChainedException(
'Failed to fetch order $orderId',
innerException: e,
);
}
}
void main() async {
try {
await fetchOrderAmount('ORD-001');
} on ChainedException catch (e) {
print(e);
}
}
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
7. Bob 场景:DataPipeline 异常处理
▶ 示例
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
:完整的异常处理流程
import 'dart:async';
// Custom exceptions
class PipelineException implements Exception {
final String message;
PipelineException(this.message);
@override
String toString() => 'PipelineException: $message';
}
class CsvParseException extends PipelineException {
final int lineNumber;
CsvParseException(String message, this.lineNumber) : super(message);
@override
String toString() => 'CsvParseException: $message (line $lineNumber)';
}
// Safe CSV parser with exception handling
List<Map<String, String>> parseCsv(String content) {
final lines = content.split('\n');
if (lines.isEmpty) throw PipelineException('Empty CSV content');
final headers = lines[0].split(',');
final records = <Map<String, String>>[];
for (var i = 1; i < lines.length; i++) {
final line = lines[i].trim();
if (line.isEmpty) continue;
try {
final values = line.split(',');
if (values.length != headers.length) {
throw CsvParseException(
'Column count mismatch: expected ${headers.length}, got ${values.length}',
i + 1,
);
}
final record = <String, String>{};
for (var j = 0; j < headers.length; j++) {
record[headers[j].trim()] = values[j].trim();
}
records.add(record);
} on CsvParseException {
rethrow;
} catch (e) {
throw CsvParseException('Unexpected error: $e', i + 1);
}
}
return records;
}
Future``<void>`` processData(String csvContent) async {
List<Map<String, String>>? records;
try {
records = parseCsv(csvContent);
print('Parsed ${records.length} records');
// Simulate API call with timeout
await Future.delayed(const Duration(seconds: 1));
print('Data submitted successfully');
} on CsvParseException catch (e) {
print('Parse error: $e - skipping malformed records');
} on TimeoutException catch (e) {
print('Network timeout: $e - will retry later');
} on PipelineException catch (e) {
print('Pipeline error: $e');
} finally {
print('Cleanup: resources released');
}
}
void main() async {
final csv = 'id,amount,status\nORD-001,1500,completed\nORD-002,50,pending\nORD-003,bad_data,completed';
await processData(csv);
print('\n--- Test with malformed CSV ---');
final badCsv = 'id,amount\nORD-001,1500,completed'; // Wrong column count
await processData(badCsv);
}
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
8. 完整示例:DataPipeline 健壮数据处理
// ============================================
// DataPipeline Robust Data Processing
// Full exception handling with custom exceptions
// ============================================
import 'dart:async';
// Error codes
enum PipelineError {
fileNotFound('E001', 'File not found'),
invalidFormat('E002', 'Invalid data format'),
networkTimeout('E003', 'Network timeout'),
validationFailed('E004', 'Validation failed');
final String code;
final String label;
const PipelineError(this.code, this.label);
}
class PipelineException implements Exception {
final PipelineError error;
final String detail;
final Exception? cause;
PipelineException(this.error, {this.detail = '', this.cause});
@override
String toString() => '[${error.code}] ${error.label}'
'${detail.isNotEmpty ? ": $detail" : ""}'
'${cause != null ? " (caused by: $cause)" : ""}';
}
// Order with validation
class Order {
final String id;
final double amount;
final String status;
Order({required this.id, required this.amount, required this.status}) {
if (id.isEmpty) {
throw PipelineException(PipelineError.validationFailed, detail: 'Order ID is empty');
}
if (amount <= 0) {
throw PipelineException(PipelineError.validationFailed, detail: 'Amount must be positive: $amount');
}
}
@override
String toString() => 'Order($id, \$${amount.toStringAsFixed(2)}, $status)';
}
// Robust order parser
class OrderParser {
final List``<PipelineException>`` _errors = [];
int parsed = 0;
int skipped = 0;
List``<PipelineException>`` get errors => List.unmodifiable(_errors);
Order? tryParse(Map<String, dynamic> data) {
try {
final order = Order(
id: (data['id'] ?? '') as String,
amount: (data['amount'] as num).toDouble(),
status: (data['status'] ?? 'unknown') as String,
);
parsed++;
return order;
} on PipelineException catch (e) {
_errors.add(e);
skipped++;
return null;
} on TypeError catch (e) {
_errors.add(PipelineException(
PipelineError.invalidFormat,
detail: 'Type mismatch in record: $e',
));
skipped++;
return null;
}
}
void printReport() {
print('Parsed: $parsed, Skipped: $skipped');
if (_errors.isNotEmpty) {
print('Errors:');
for (final e in _errors) {
print(' $e');
}
}
}
}
void main() {
final rawData = <Map<String, dynamic>>[
{'id': 'ORD-001', 'amount': 1500.0, 'status': 'completed'},
{'id': '', 'amount': 500.0, 'status': 'pending'}, // Invalid: empty ID
{'id': 'ORD-003', 'amount': -50.0, 'status': 'completed'}, // Invalid: negative amount
{'id': 'ORD-004', 'amount': 'not_a_number', 'status': 'pending'}, // Invalid: wrong type
{'id': 'ORD-005', 'amount': 3200.0, 'status': 'completed'},
];
final parser = OrderParser();
final validOrders = ``<Order>``[];
for (final data in rawData) {
final order = parser.tryParse(data);
if (order != null) validOrders.add(order);
}
print('=== DataPipeline Processing Report ===');
parser.printReport();
print('\nValid Orders:');
for (final order in validOrders) {
print(' $order');
}
final totalRevenue = validOrders.fold``<double>``(0, (s, o) => s + o.amount);
print('\nTotal Revenue: \$${totalRevenue.toStringAsFixed(2)} USD');
}
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
输出:
=== DataPipeline Processing Report ===
Parsed: 3, Skipped: 2
Errors:
[E004] Validation failed: Order ID is empty
[E004] Validation failed: Amount must be positive: -50.0
[E002] Invalid data format: Type mismatch in record: ...
Valid Orders:
Order(ORD-001, $1500.00, completed)
Order(ORD-005, $3200.00, completed)
Total Revenue: $4700.00 USD
❓ 常见问题
Q:Dart 有 checked exception 吗? A:没有。Dart 的异常都是 unchecked,编译器不强制要求声明或捕获。这提供了灵活性,但也要求开发者自觉处理异常。
Q:on 和 catch 有什么区别? A:
on Type捕获特定类型的异常但不绑定变量(除非加 catch);catch (e)捕获所有异常并绑定变量。通常用on Type catch (e)组合。
Q:catch 和 rethrow 有什么用? A:catch 捕获异常后可以记录日志、执行清理,然后用 rethrow 重新抛出,让上层继续处理。rethrow 保留原始堆栈跟踪,比 throw e 更好。
Q:finally 块什么时候执行? A:无论 try 块是否抛出异常、catch 是否捕获、是否有 return,finally 都会执行。唯一例外是程序被杀死(如 SIGKILL)。
Q:自定义异常应该继承什么? A:推荐 implements Exception 而非 extends Exception。implements 不受单继承限制,更灵活。Dart 官方也推荐这种方式。
Q:异常处理对性能有影响吗? A:try-catch 块本身几乎零开销(Dart VM 不在 try 块添加额外指令)。但异常的创建和抛出有成本,不应把异常当作正常流程控制。
Q:如何避免吞掉异常? A:空 catch 块是代码坏味道。至少加日志记录,或者 rethrow。如果确实要忽略,用
catch (_) {}并加注释说明原因。
📖 小节
- Exception 可恢复应捕获,Error 不可恢复不应捕获
- try-on-catch-finally 完整语法:on 按类型捕获,catch 绑定变量,finally 始终执行
- 自定义异常 implements Exception,携带上下文信息(行号、字段、错误码)
- rethrow 保留原始堆栈,异常链追踪根因
- DataPipeline 用 tryParse 模式:单条记录失败不影响整批处理
📝 作业
- 基础题(难度⭐):写一个函数
safeParseInt(String s),在 try-catch 中调用 int.parse,解析失败时返回 0 而不是抛异常。测试 3 个用例。 - 进阶题(难度⭐⭐):定义
DataFormatException自定义异常类,携带字段名、无效值和行号。写一个 CSV 解析函数,在格式错误时抛出该异常,并在调用处捕获打印详细信息。 - 挑战题(难度⭐⭐⭐):实现一个带重试机制的 HTTP 请求函数,支持自定义重试次数和退避策略。遇到 TimeoutException 重试,超过次数后抛出包含所有重试异常的聚合异常。