Dart: Dart 异常处理 — 编写健壮的错误处理代码

异常处理是代码的安全网 — 没有它,一个错误就能让整个系统崩溃。

1. 你将学到


2. 一个开发者的真实故事

(1) 痛点:未处理的异常让批处理任务中途崩溃

Bob 的 DataPipeline 在处理百万级订单时,因为一个格式错误的 CSV 行导致 FormatException,整个处理任务崩溃。800,000 条已处理的数据全部丢失,需要从头重跑。更糟的是,错误信息只显示 "FormatException",没有行号和上下文,Bob 花了 4 小时才定位到问题。

(2) 异常处理的解法

用 try-on-catch-finally 捕获特定异常,自定义异常类携带上下文信息,finally 确保资源释放。

DART
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();
}
TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

(3) 收益


3. Exception vs Error

(1) 语义区别

100%
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]
TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
维度 Error Exception
可恢复性 不可恢复 可恢复
应捕获否 不应该 应该
产生者 VM / 运行时 应用代码
示例 StackOverflowError FormatException
⚠️ 注意: 不要捕获 Error。Error 表示程序状态已损坏,捕获后继续执行可能导致更严重的问题。只捕获 Exception。


4. try / on / catch / finally

(1) 完整语法

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

:基本 try-catch

DART
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');
  }
}
TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

:on 与 catch 的区别

DART
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');
  }
}
TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

:finally 块

DART
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');
  }
}
TEXT 📖 仅展示
> **输出:** 在本地 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) 异常类设计原则

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

:自定义异常类

DART
// 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';
}
TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

:错误码设计

DART
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
  }
}
TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

6. rethrow 与异常链

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

:rethrow

DART
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');
  }
}
TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

:异常链

DART
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);
  }
}
TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

7. Bob 场景:DataPipeline 异常处理

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

:完整的异常处理流程

DART
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);
}
TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

8. 完整示例:DataPipeline 健壮数据处理

DART
// ============================================
// 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');
}
TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

输出:

TEXT 📖 仅展示
=== 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 (_) {} 并加注释说明原因。


📖 小节


📝 作业

  1. 基础题(难度⭐):写一个函数 safeParseInt(String s),在 try-catch 中调用 int.parse,解析失败时返回 0 而不是抛异常。测试 3 个用例。
  2. 进阶题(难度⭐⭐):定义 DataFormatException 自定义异常类,携带字段名、无效值和行号。写一个 CSV 解析函数,在格式错误时抛出该异常,并在调用处捕获打印详细信息。
  3. 挑战题(难度⭐⭐⭐):实现一个带重试机制的 HTTP 请求函数,支持自定义重试次数和退避策略。遇到 TimeoutException 重试,超过次数后抛出包含所有重试异常的聚合异常。

← 上一课 | 下一课 →

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏