异常

Exception

异常的基本概念与抛出(throw)

理论

在Dart中,异常(Exception)是指程序在运行时发生的非预期的错误情况,它会中断程序的正常执行流程。当一个异常发生时,如果它没有被捕获处理,程序就会终止并报告错误。

Dart中的异常处理机制允许我们优雅地处理这些错误,而不是让程序崩溃。

要手动触发或 抛出(throw)一个异常,我们可以使用throw关键字,后跟任意对象。通常,我们会抛出Exception类或其子类的实例,或者Error类或其子类的实例。

注意: 实践中,我们通常抛出那些扩展自ExceptionError的对象,以提供更结构化的错误信息。

示例

// main.dart
// Dart SDK 版本: 2.19.0 或更高
// 运行环境要求: Dart VM

void checkAge(int age) {
  if (age < 0) {
    // 抛出一个 ArgumentError 异常。ArgumentError 是 Error 的一个子类。
    throw ArgumentError('年龄不能为负数。Received age: $age'); // 英文原文:Argument Error
  } else if (age < 18) {
    // 抛出一个FormatException异常。FormatException 是 Exception 的一个子类。
    throw FormatException('年龄太小不允许访问。Received age: $age'); // 英文原文:Format Exception
  } else {
    print('年龄:$age,访问已授权。');
  }
}

void main() {
  print('--- 尝试有效年龄 ---');
  checkAge(20);

  print('\n--- 尝试负数年龄 (将抛出异常) ---');
  try {
    checkAge(-5); // 这里的异常会中断后续代码执行,但我们可以try-catch捕获
  } catch (e) {
    print('捕获到异常(主函数中):${e.runtimeType} - $e');
  }

  print('\n--- 尝试过小年龄 (将抛出异常) ---');
  try {
    checkAge(10); // 这里的异常会中断后续代码执行,但我们可以try-catch捕获
  } catch (e) {
    print('捕获到异常(主函数中):${e.runtimeType} - $e');
  }

  print('\n程序执行完毕。');
}

运行结果

--- 尝试有效年龄 ---
年龄:20,访问已授权。

--- 尝试负数年龄 (将抛出异常) ---
捕获到异常(主函数中):ArgumentError - Invalid argument(s): 年龄不能为负数。Received age: -5

--- 尝试过小年龄 (将抛出异常) ---
捕获到异常(主函数中):FormatException - FormatException: 年龄太小不允许访问。Received age: 10

程序执行完毕。

注意事项

  • 未捕获的异常会导致程序终止: 如果一个异常被抛出,但没有被任何try-catch块捕获,程序将立即停止执行。
  • throw可以抛出任何对象: 虽然可以抛出任何对象,但强烈建议抛出ExceptionError的子类,这有助于接收方更好地理解和处理错误类型。
  • 异常是运行时错误: 异常与编译时错误不同,编译时错误会在代码编译阶段被发现,而异常在程序运行时才可能发生。

捕获与处理异常(try-on-catch)

理论

为了防止异常导致程序崩溃,我们需要捕获(catch)它们。Dart提供了tryoncatch关键字来处理异常。

  • try块: 包含可能抛出异常的代码。
  • on关键字: 用于指定要捕获的特定异常类型。如果try块中发生了指定类型的异常,on块将被执行。
  • catch关键字: 用于捕获任何类型的异常,或者与on结合使用以获取异常对象本身以及堆栈跟踪。

语法结构

try {
  // 可能会抛出异常的代码
} on SpecificExceptionType {
  // 处理特定类型的异常
} on AnotherSpecificExceptionType catch (e) {
  // 处理另一种特定类型的异常,并访问异常对象 e
} catch (e) {
  // 捕获所有其他类型的异常,并访问异常对象 e
} catch (e, s) {
  // 捕获所有其他类型的异常,并访问异常对象 e 和堆栈跟踪 s
}

注意: on块和catch块可以单独使用,也可以组合使用。

  • on只能捕获特定类型的异常。
  • catch可以捕获任何异常,并提供异常对象。
  • on SpecificExceptionType catch (e)结合了两者的功能,捕获特定类型异常并获取异常对象。
  • 异常捕获的顺序很重要,更具体的异常类型应该放在前面,更通用的异常类型放在后面。

示例

// main.dart
// Dart SDK 版本: 2.19.0 或更高
// 运行环境要求: Dart VM

void riskyOperation(int value) {
  if (value < 0) {
    throw ArgumentError('值不能为负数。');
  } else if (value == 0) {
    throw StateError('值不能为零,因为它会导致一个无效状态。'); // 英文原文:State Error
  } else if (value > 100) {
    throw Exception('值过大,超出处理范围。'); // 英文原文:Exception
  }
  print('操作成功,值为: $value');
}

void main() {
  print('--- 尝试有效操作 ---');
  try {
    riskyOperation(50);
  } on ArgumentError catch (e) {
    print('捕获到 ArgumentError: $e'); // 这不会被触发
  } catch (e, stackTrace) {
    print('捕获到通用异常: ${e.runtimeType} - $e');
    print('堆栈跟踪: $stackTrace'); // 这不会被触发
  }

  print('\n--- 尝试负数 (捕获 ArgumentError) ---');
  try {
    riskyOperation(-1);
  } on ArgumentError catch (e) { // 捕获特定类型的ArgumentError
    print('捕获到 ArgumentError: $e');
  } on StateError catch (e) {
    print('捕获到 StateError: $e');
  } catch (e, s) { // 捕获其他所有异常
    print('捕获到通用异常: ${e.runtimeType} - $e');
    print('堆栈跟踪: $s');
  }

  print('\n--- 尝试零 (捕获 StateError) ---');
  try {
    riskyOperation(0);
  } on ArgumentError catch (e) {
    print('捕获到 ArgumentError: $e');
  } on StateError catch (e) { // 捕获特定类型的StateError
    print('捕获到 StateError: $e');
  } catch (e, s) {
    print('捕获到通用异常: ${e.runtimeType} - $e');
    print('堆栈跟踪: $s');
  }

  print('\n--- 尝试过大值 (捕获通用 Exception) ---');
  try {
    riskyOperation(200);
  } on ArgumentError catch (e) {
    print('捕获到 ArgumentError: $e');
  } on StateError { // 可以只用on,不捕获异常对象
    print('捕获到 StateError (没有异常对象)');
  } catch (e, s) { // 捕获所有其他异常,包括 Exception
    print('捕获到通用异常: ${e.runtimeType} - $e');
    print('堆栈跟踪: $s');
  }

  print('\n程序执行完毕。');
}

运行结果

--- 尝试有效操作 ---
操作成功,值为: 50

--- 尝试负数 (捕获 ArgumentError) ---
捕获到 ArgumentError: Invalid argument(s): 值不能为负数。

--- 尝试零 (捕获 StateError) ---
捕获到 StateError: Bad state: 值不能为零,因为它会导致一个无效状态。

--- 尝试过大值 (捕获通用 Exception) ---
捕获到通用异常: Exception - Exception: 值过大,超出处理范围。
堆栈跟踪: #0      riskyOperation (file:///.../main.dart:20:5)
#1      main (file:///.../main.dart:67:5)
#2      _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:297:19)
#3      _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:184:12)

程序执行完毕。

注意事项

  • 异常捕获的顺序: 总是将最具体的异常类型放在on块的前面,将更通用的异常类型放在后面。如果catch (e)放在最前面,它会捕获所有异常,导致后面的on SpecificExceptionType永不被执行。
  • stackTrace的作用: stackTrace对象提供了异常发生时的方法调用序列,这对于调试非常有用,尤其是在生产环境中记录错误时。
  • 资源清理: 即使发生异常,有时也需要确保某些资源(如文件句柄、网络连接)被关闭。这正是finally块的用武之地。

finally块

理论

finally块包含的代码无论try块中是否发生异常,也无论异常是否被捕获,都会在trycatch块执行完毕后执行。它通常用于执行清理操作,例如关闭文件、释放资源或取消网络请求。

语法结构

try {
  // 可能会抛出异常的代码
} on SpecificExceptionType {
  // 处理特定类型的异常
} catch (e) {
  // 捕获所有其他类型的异常
} finally {
  // 无论是否发生异常,这部分代码总会执行
}

注意: 即使try块中通过return语句提前退出,或者抛出一个未被捕获的异常,finally块也依然会执行。

示例

// main.dart
// Dart SDK 版本: 2.19.0 或更高
// 运行环境要求: Dart VM

void processFile(String fileName, String content) {
  // 模拟文件操作
  print('开始处理文件: $fileName');
  bool fileOpened = false; // 模拟资源(文件)是否被打开

  try {
    print('尝试打开文件...');
    fileOpened = true; // 模拟文件已打开

    if (fileName.isEmpty) {
      throw FormatException('文件名不能为空。');
    }
    if (content.length > 20) {
      throw Exception('文件内容过长,当前限制20字符。');
    }

    print('文件内容写入成功: "$content"');
  } on FormatException catch (e) {
    print('捕获到文件名格式错误: $e');
  } catch (e) {
    print('捕获到通用文件处理异常: ${e.runtimeType} - $e');
  } finally {
    // 无论是否发生异常,文件都应该关闭
    if (fileOpened) {
      print('文件已关闭。');
    } else {
      print('文件未被打开,无需关闭。');
    }
    print('文件处理流程结束。');
  }
}

void main() {
  print('--- 正常文件处理 ---');
  processFile('report.txt', '这是报告内容');

  print('\n--- 文件名为空 (FormatException) ---');
  processFile('', '一些内容');

  print('\n--- 内容过长 (Exception) ---');
  processFile('long_data.txt', '这是一个非常非常长的内容,超过了20个字符限制');

  print('\n程序执行完毕。');
}

运行结果

--- 正常文件处理 ---
开始处理文件: report.txt
尝试打开文件...
文件内容写入成功: "这是报告内容"
文件已关闭。
文件处理流程结束。

--- 文件名为空 (FormatException) ---
开始处理文件: 
尝试打开文件...
捕获到文件名格式错误: FormatException: 文件名不能为空。
文件已关闭。
文件处理流程结束。

--- 内容过长 (Exception) ---
开始处理文件: long_data.txt
尝试打开文件...
捕获到通用文件处理异常: Exception - Exception: 文件内容过长,当前限制20字符。
文件已关闭。
文件处理流程结束。

程序执行完毕。

注意事项

  • 确保资源释放: finally块是执行清理逻辑的最佳位置,例如关闭数据库连接、网络套接字或文件流。
  • finally不改变异常流程: finally块的执行不会改变try块中异常的传播行为。如果异常未被catch捕获,它会在finally块执行完毕后继续向上层传播。
  • finally中抛出异常: 如果在finally块中再次抛出异常,这个新异常会覆盖try块(或catch块)中可能抛出的任何未处理的异常。通常应避免在finally块中抛出异常。

自定义异常

理论

Dart允许我们创建自己的自定义异常类型。这在需要表示应用程序特有的错误条件时非常有用,它能提高代码的可读性、可维护性,并允许我们进行更精细的错误处理。

自定义异常通常是通过继承Exception类(或者更具体的如FormatExceptionStateError等)来实现的。继承Error类也是一种选择,但通常Error表示程序更严重的、不应该被捕获的编程错误(稍后解释)。

创建自定义异常的步骤:

  1. 创建一个新的类。
  2. 让这个类继承Exception或其它合适的内置异常类。
  3. 根据需要添加构造函数和字段,以提供详细的错误信息。
  4. 重写toString()方法以提供友好的异常描述。

示例

// domain_exceptions.dart
// 这是自定义异常的定义文件
// Dart SDK 版本: 2.19.0 或更高

/// 表示用户认证失败的自定义异常。
class AuthenticationException implements Exception {
  final String message;
  final String? errorCode; // 可选的错误码

  AuthenticationException(this.message, {this.errorCode});

  
  String toString() {
    return 'AuthenticationException: $message' +
           (errorCode != null ? ' (Code: $errorCode)' : '');
  }
}

/// 表示数据格式错误的自定义异常。
class InvalidDataException implements Exception {
  final String fieldName;
  final String invalidValue;
  final String? expectedFormat;

  InvalidDataException(this.fieldName, this.invalidValue, {this.expectedFormat});

  
  String toString() {
    return 'InvalidDataException: 字段 "$fieldName" 的值 "$invalidValue" 无效。' +
           (expectedFormat != null ? ' 期望格式: $expectedFormat' : '');
  }
}

// main.dart
// Dart SDK 版本: 2.19.0 或更高
// 运行环境要求: Dart VM
import 'domain_exceptions.dart'; // 导入自定义异常

void performLogin(String username, String password) {
  if (username.isEmpty || password.isEmpty) {
    throw InvalidDataException('username/password', 'empty', expectedFormat: '非空字符串');
  }
  if (username != 'admin' || password != 'password123') {
    throw AuthenticationException('用户名或密码不正确', errorCode: 'AUTH_001');
  }
  print('用户 "$username" 登录成功!');
}

void main() {
  print('--- 尝试有效登录 ---');
  try {
    performLogin('admin', 'password123');
  } catch (e) {
    print('捕获到异常: $e'); // 这不会被触发
  }

  print('\n--- 尝试空用户名 (InvalidDataException) ---');
  try {
    performLogin('', 'password');
  } on InvalidDataException catch (e) {
    print('捕获到自定义异常 (InvalidDataException): $e');
  } on AuthenticationException catch (e) {
    print('捕获到自定义异常 (AuthenticationException): $e');
  } catch (e, s) {
    print('捕获到通用异常: ${e.runtimeType} - $e');
    print('堆栈跟踪: $s');
  }

  print('\n--- 尝试错误密码 (AuthenticationException) ---');
  try {
    performLogin('admin', 'wrong_password');
  } on InvalidDataException catch (e) {
    print('捕获到自定义异常 (InvalidDataException): $e');
  } on AuthenticationException catch (e) {
    print('捕获到自定义异常 (AuthenticationException): $e');
  } catch (e, s) {
    print('捕获到通用异常: ${e.runtimeType} - $e');
    print('堆栈跟踪: $s');
  }

  print('\n程序执行完毕。');
}

运行结果

--- 尝试有效登录 ---
用户 "admin" 登录成功!

--- 尝试空用户名 (InvalidDataException) ---
捕获到自定义异常 (InvalidDataException): InvalidDataException: 字段 "username/password" 的值 "empty" 无效。 期望格式: 非空字符串

--- 尝试错误密码 (AuthenticationException) ---
捕获到自定义异常 (AuthenticationException): AuthenticationException: 用户名或密码不正确 (Code: AUTH_001)

程序执行完毕。

注意事项

  • 为业务逻辑创建: 自定义异常应该用于表示应用程序的业务逻辑错误,而不是程序内部的缺陷。
  • 提供足够的信息: 在自定义异常中包含足够的信息(如错误消息、错误码、导致错误的数据等),以便在捕获时能够进行有效的诊断和处理。
  • 继承Exception: 大多数情况下,自定义异常应该继承Exception而不是ErrorError通常用于表示程序无法恢复的编程错误。
  • 实现toString(): 重写toString()方法使得异常在打印时能够提供有意义的信息。

异步操作中的异常处理

理论

在Dart中,异步编程是核心特性,主要通过 Future 和 Stream 来实现。异步操作中的异常处理方式与同步操作有所不同。

  • 对于Future: 当一个Future失败时(即发生异常时),它会以一个错误(error)而非一个值(value)来完成。

    • 你可以使用try-catch块来捕获await表达式之后抛出的异常。
    • 或者,对于不使用awaitFuture,可以使用Future.catchError方法来处理异常。
  • 对于Stream: Stream是一系列异步事件。当Stream处理过程中发生异常时,它会触发onError事件。

    • 你可以监听Streamlisten方法,并在其onError回调中处理异常。
    • 使用await for循环时,可以直接在try-catch块中捕获Stream抛出的异常。

注意: 任何未捕获的Future异常最终都会被Zone捕获(如果没有设置自定义Zone,应用程序通常会终止)。

示例

// main.dart
// Dart SDK 版本: 2.19.0 或更高
// 运行环境要求: Dart VM

// --- Future 异常处理 ---

/// 模拟一个可能失败的异步操作
Future<String> fetchData(bool shouldFail) async {
  await Future.delayed(Duration(milliseconds: 500)); // 模拟网络延迟
  if (shouldFail) {
    throw Exception('数据获取失败:网络连接错误或服务器无响应');
  }
  return '成功获取到异步数据!';
}

/// 模拟另一个异步操作,可能返回特定错误
Future<int> processData(String rawData) async {
  await Future.delayed(Duration(milliseconds: 300));
  if (rawData.contains('error')) {
    throw FormatException('数据格式不正确');
  }
  return rawData.length;
}

// --- Stream 异常处理 ---

/// 模拟一个可能抛出异常的 Stream
Stream<int> countWithErrors(int maxCount, {required bool shouldError}) async* {
  for (int i = 0; i < maxCount; i++) {
    await Future.delayed(Duration(milliseconds: 100));
    if (i == 3 && shouldError) {
      throw StateError('Stream 在第三次迭代时发生错误');
    }
    yield i;
  }
}

Future<void> main() async {
  print('--- Future 异常处理 (使用 await 和 try-catch) ---');
  try {
    String result = await fetchData(true); // 将会失败
    print(result);
  } on Exception catch (e) {
    print('Future 1 捕获到异常 (await): ${e.runtimeType} - $e');
  }

  print('\n--- Future 异常处理 (使用 .catchError) ---');
  fetchData(false).then((data) { // 将会成功
    print('Future 2 获取数据成功: $data');
  }).catchError((e) {
    print('Future 2 捕获到异常 (catchError): ${e.runtimeType} - $e'); // 这不会被触发
  });

  fetchData(true).then((data) { // 将会失败,使用 .catchError
    print('Future 3 获取数据成功: $data'); // 这不会被触发
  }).catchError((e) {
    print('Future 3 捕获到异常 (catchError): ${e.runtimeType} - $e');
  }).whenComplete(() {
    print('Future 3 处理完成 (无论成功失败)。'); // finally 块的异步版本
  });

  print('\n--- 组合 Future 异常处理 ---');
  try {
    String data = await fetchData(false);
    int processedLength = await processData('valid data');
    print('第一步数据: $data, 第二步处理长度: $processedLength');

    data = await fetchData(false);
    processedLength = await processData('error data'); // 将会抛出FormatException
    print('第一步数据: $data, 第二步处理长度: $processedLength'); // 不会执行
  } on FormatException catch (e) {
    print('组合操作捕获到 FormatException: $e');
  } on Exception catch (e) {
    print('组合操作捕获到通用 Exception: $e');
  }


  print('\n--- Stream 异常处理 (使用 listen) ---');
  countWithErrors(5, shouldError: true).listen(
    (data) {
      print('Stream listen 接收到数据: $data');
    },
    onError: (e, s) { // Stream 的 onError 回调
      print('Stream listen 捕获到异常: ${e.runtimeType} - $e');
      print('堆栈跟踪: $s');
    },
    onDone: () {
      print('Stream listen 处理完成。');
    },
    cancelOnError: true, // 遇到错误时取消订阅
  );

  print('\n--- Stream 异常处理 (使用 await for 和 try-catch) ---');
  try {
    await for (var num in countWithErrors(5, shouldError: true)) {
      print('await for 循环接收到数据: $num');
    }
  } on StateError catch (e, s) {
    print('await for 循环捕获到 StateError: $e');
    print('堆栈跟踪: $s');
  } catch (e, s) {
    print('await for 循环捕获到通用异常: ${e.runtimeType} - $e');
    print('堆栈跟踪: $s');
  }
  
  await Future.delayed(Duration(seconds: 2)); // 等待所有异步操作完成
  print('\n所有异步操作示例完成。');
}

运行结果

--- Future 异常处理 (使用 await 和 try-catch) ---
Future 1 捕获到异常 (await): Exception - Exception: 数据获取失败:网络连接错误或服务器无响应

--- Future 异常处理 (使用 .catchError) ---
Future 2 获取数据成功: 成功获取到异步数据!
Future 3 捕获到异常 (catchError): Exception - Exception: 数据获取失败:网络连接错误或服务器无响应
Future 3 处理完成 (无论成功失败)。

--- 组合 Future 异常处理 ---
第一步数据: 成功获取到异步数据!, 第二步处理长度: 10
组合操作捕获到 FormatException: FormatException: Bad format: 数据格式不正确

--- Stream 异常处理 (使用 listen) ---
Stream listen 接收到数据: 0
Stream listen 接收到数据: 1
Stream listen 接收到数据: 2
Stream listen 捕获到异常: StateError - Bad state: Stream 在第三次迭代时发生错误
堆栈跟踪: #0      countWithErrors (file:///.../main.dart:45:7)
#1      _StreamImpl.skipWhile.<anonymous closure>.<anonymous closure> (package:async/src/stream/stream_impl.dart:36:20)
#2      _StreamImpl._runGuarded (package:async/src/stream/stream_impl.dart:18:24)
#3      _DelayFuture._complete (dart:async/future_impl.dart:1033:23)
...
Stream listen 处理完成。

--- Stream 异常处理 (使用 await for 和 try-catch) ---
await for 循环接收到数据: 0
await for 循环接收到数据: 1
await for 循环接收到数据: 2
await for 循环捕获到 StateError: Bad state: Stream 在第三次迭代时发生错误
堆栈跟踪: #0      countWithErrors (file:///.../main.dart:46:7)
#1      main (file:///.../main.dart:110:17)
#2      _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:297:19)
#3      _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:184:12)

所有异步操作示例完成。

注意事项

  • async/awaittry-catch: 在async函数中使用await表达式时,其抛出的异常可以像同步代码一样被try-catch捕获。这是处理Future异常最推荐和最易读的方式。
  • Future.catchError: 当不使用await或在链式调用Future时,catchError是捕获异常的有效方式。它有一个test参数可以用于过滤特定异常类型。
  • Future.whenComplete: 类似于同步代码的finally块,whenComplete无论Future成功还是失败,都会执行。
  • StreamControlleraddError: 如果你在创建Stream,可以使用StreamController.addError()方法来显式地向Stream发送一个错误事件。
  • Zone的作用: 在Flutter或大型Dart应用中,Zone提供了一个机制来全局捕获未处理的异步异常,这对于错误报告和崩溃日志尤其有用。例如,Flutter的runZonedGuarded就是基于Zone提供的。

Error与Exception的区别

理论

在Dart中,ErrorException都是用于表示程序中出现问题的类,但它们代表了不同类型的问题,并暗示了不同的处理策略。

  • Error(错误):

    • 通常表示程序中的逻辑缺陷或运行时不变量被违反。
    • 这类错误通常是程序员的错误,例如:
      • AssertionError(断言失败)
      • NullThrownError(抛出了null)
      • StackOverflowError(栈溢出)
      • OutOfMemoryError(内存不足)
      • ArgumentError(方法接收到非法参数)
      • StateError(对象处于非法状态,例如在一个已关闭的流上写入)
    • Error通常是不可恢复的。这意味着我们不应该尝试catch Error并继续执行应用程序。如果一个Error发生,通常程序应该终止,并通过修复代码来解决问题。
    • 继承自Error的类通常不实现Exception接口。
  • Exception(异常):

    • 通常表示程序在运行时遇到的、预期可能发生的、但又不属于正常流程的意外情况。
    • 这类情况通常是程序外部因素导致的,例如:
      • 网络连接失败
      • 文件找不到
      • 用户的无效输入
      • JSON解析失败(FormatException)
    • Exception通常是可恢复的。这意味着你可以catch Exception并采取适当的措施(如重试操作、向用户显示错误消息、记录日志等),然后程序可以继续执行。
    • Exception类实现了Exception接口(在Dart中,Exception本身就是基类,不是接口)。
特征ErrorException
含义程序内部的编程错误或系统资源耗尽程序可预见的、可恢复的运行时问题
原因程序员的逻辑错误、内存不足、栈溢出等网络问题、文件不存在、无效的用户输入、API返回错误等
恢复性不可恢复,通常表示程序需要终止并修复代码可恢复,可以捕获并优雅处理,程序可继续运行
例子ArgumentErrorStateErrorNullThrownErrorFormatExceptionIOExceptionTimeoutException
处理策略不应捕获,应终止程序并进行代码调试应捕获并处理以保持程序健壮性

示例

// main.dart
// Dart SDK 版本: 2.19.0 或更高
// 运行环境要求: Dart VM

// 模拟函数,可能抛出 ArgumentError (Error的子类)
void processValue(Object? value) {
  if (value == null) {
    // 这是一个典型的编程错误:不应该将 null 传入需要非空值的地方
    throw ArgumentError('值不能为 null。'); // ArgumentError 是 Error 的子类
  }
  if (!(value is String)) {
    // 假设此函数只处理字符串
    throw ArgumentError('期望字符串类型,收到 ${value.runtimeType}。');
  }
  print('处理值成功: $value');
}

// 模拟函数,可能抛出 FormatException (Exception的子类)
void parseNumber(String text) {
  try {
    int number = int.parse(text); // 可能会抛出 FormatException
    print('解析数字成功: $number');
  } on FormatException catch (e) {
    // 这是一个可预期的用户输入错误,可以捕获并进行处理
    print('捕获到 FormatException (可恢复): $e - 输入 "$text" 不是有效的数字格式。');
    // 可以进行重试、提示用户重新输入等操作
  }
}

void main() {
  print('--- 处理值 (ArgumentError - 不应捕获) ---');
  try {
    processValue('hello');
    processValue(123); // 会抛出 ArgumentError (期望 String)
  } catch (e) {
    // 虽然这里捕获了,但在实际生产代码中,不应为了恢复而捕获 ArgumentError
    print('捕获到异常: ${e.runtimeType} - $e');
  }

  print('\n--- 解析数字 (FormatException - 应捕获并恢复) ---');
  parseNumber('123');
  parseNumber('abc'); // 会抛出 FormatException,但我们捕获并处理了
  parseNumber('45.6'); // 也会抛出 FormatException

  // 演示未捕获的 Error 导致程序终止
  print('\n--- 演示未捕获的 Error (将会使程序终止) ---');
  try {
    List<int> numbers = [1, 2, 3];
    print(numbers[5]); // IndexError 是 Error 的子类,通常不应该捕获
  } catch (e) {
    print('捕获了 Index Error (但通常不建议这样做): ${e.runtimeType} - $e');
    // 即使在这里捕获,也表明程序存在逻辑错误,需要修复
  }
  
  // 如果这里再 `throw IndexError('...')` 并且不捕获,程序就会终止
  
  print('\n程序执行完毕。'); // 如果有未捕获的 Error,这行可能不会被执行
}

运行结果

--- 处理值 (ArgumentError - 不应捕获) ---
处理值成功: hello
捕获到异常: ArgumentError - Invalid argument(s): 期望字符串类型,收到 int。

--- 解析数字 (FormatException - 应捕获并恢复) ---
解析数字成功: 123
捕获到 FormatException (可恢复): FormatException: Invalid number (at character 1)
abc
^
 - 输入 "abc" 不是有效的数字格式。
捕获到 FormatException (可恢复): FormatException: Invalid number (at character 3)
45.6
  ^
 - 输入 "45.6" 不是有效的数字格式。

--- 演示未捕获的 Error (将会使程序终止) ---
捕获了 Index Error (但通常不建议这样做): RangeError - RangeError (index): Invalid value: Not in inclusive range 0..2: 5

程序执行完毕。

如果上面numbers[5]try-catch被移除,或者没有捕获RangeError,则程序的输出会是

--- 处理值 (ArgumentError - 不应捕获) ---
处理值成功: hello
捕获到异常: ArgumentError - Invalid argument(s): 期望字符串类型,收到 int。

--- 解析数字 (FormatException - 应捕获并恢复) ---
解析数字成功: 123
捕获到 FormatException (可恢复): FormatException: Invalid number (at character 1)
abc
^
 - 输入 "abc" 不是有效的数字格式。
捕获到 FormatException (可恢复): FormatException: Invalid number (at character 3)
45.6
  ^
 - 输入 "45.6" 不是有效的数字格式。

--- 演示未捕获的 Error (将会使程序终止) ---
Unhandled exception:
RangeError (index): Invalid value: Not in inclusive range 0..2: 5
#0      List.[] (dart:core-patch/growable_array.dart:257:3)
#1      main (file:///.../main.dart:58:18)
#2      _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:297:19)
#3      _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:184:12)

可以看到,当RangeError未被捕获时,程序直接终止。

注意事项

  • 不要轻易捕获Error: 通常不建议使用try-catch来捕获Error及其子类,因为它们表明程序存在缺陷,捕获它们会掩盖真正的问题,并可能导致程序处于不确定状态。正确的做法是修复代码以避免这些Error发生。
  • 捕获Exception是为了恢复: 捕获Exception是为了处理预期的运行时问题,并允许程序从这些问题中恢复或优雅地失败。
  • 如何区分ErrorException: 当你遇到一个问题时,思考它是否是由于你的代码逻辑错误导致的(例如不正确的参数传递、无效状态),如果是,那它更偏向于Error。如果它是由于外部环境或用户行为导致的(例如网络断开、文件权限不足、用户输入格式不正确),那它更偏向于Exception
  • AssertionError: Dart的assert语句会在开发模式下抛出AssertionError。在生产模式下,assert语句会被忽略,不会抛出异常。

实践指南与最佳实践

理论

良好地处理异常是构建健壮应用程序的关键。以下是一些在Dart中处理异常的实践指南和最佳实践:

  1. 只捕获你知道如何处理的异常: 不要无差别地捕获所有异常(catch (e)),除非你真的知道如何处理所有这些异常,或者你只是想记录它们。更具体的on语句通常更好。
  2. 避免空catch块: 一个空的catch块会吞噬异常,使你无法知道发生了什么问题,并且难以调试。至少应该记录异常或重新抛出它。
  3. 重新抛出异常(rethrow): 如果你捕获了一个异常,但发现自己无法完全处理它,或者希望上层调用者也能知道这个异常,你可以使用rethrow关键字。rethrow会重新抛出原始异常,并保留其原始堆栈跟踪。
  4. 提供有意义的错误信息: 无论是在抛出自定义异常还是记录内置异常时,确保错误消息清晰、具体,并包含足够的信息来诊断问题。
  5. 为用户提供友好反馈: 在用户界面层,将技术性异常转换为用户友好的消息,告知用户发生了什么,以及他们可以尝试什么(如果可以)。
  6. 日志记录: 始终将捕获到的异常及其堆栈跟踪记录到日志系统,特别是在生产环境中。这对于后期分析和调试至关重要。
  7. 异步异常的Zone处理: 在Flutter或大型Dart应用中,使用runZonedGuarded来全局捕获所有未处理的异步异常,防止应用崩溃,并集中处理错误报告。
  8. 考虑使用Result类型(函数式编程风格): 对于某些场景,特别是纯函数,与其抛出异常,不如返回一个表示成功值或失败错误的Result对象。这使得函数签名清晰地表明了两种可能的返回状态,而不会中断正常的控制流。这是一种替代异常的错误处理方式,尤其适用于库或API设计。

示例

// main.dart
// Dart SDK 版本: 2.19.0 或更高
// 运行环境要求: Dart VM

import 'dart:io';

// 模拟一个依赖于文件读取的函数
Future<String> readFileContent(String filePath) async {
  try {
    File file = File(filePath);
    if (!await file.exists()) {
      // 文件不存在是预期但需要处理的场景,抛出自定义异常更合适
      throw FileSystemException('文件不存在', filePath);
    }
    String content = await file.readAsString();
    if (content.isEmpty) {
      throw FileSystemException('文件内容为空', filePath);
    }
    return content;
  } on FileSystemException catch (e, s) {
    // 捕获特定的文件系统异常,并添加更多上下文信息
    print('[LOG] 警告: 文件系统操作失败,路径: $filePath - ${e.message}');
    // 重新抛出,让上层知道这个异常
    rethrow;
  } on PathNotFoundException catch (e) {
    // 针对PathNotFoundException这种更具体的异常进行处理
    print('[LOG] 错误: 指定路径找不到 - ${e.message}');
    rethrow;
  } catch (e, s) {
    // 捕获所有其他未知异常,并记录详细信息
    print('[LOG] 严重错误: 读取文件时发生未知异常: ${e.runtimeType} - $e');
    print('[LOG] 堆栈跟踪: $s');
    rethrow; // 重新抛出,如果这里不处理,让上层决定如何应对
  } finally {
    print('文件读取尝试完成 ($filePath)。');
  }
}

// Result 类型示例 (函数式错误处理,替代异常)
// 定义一个 Result 类型,表示成功或失败
sealed class Result<T, E> {
  const Result();
}

class Success<T, E> extends Result<T, E> {
  final T value;
  const Success(this.value);
}

class Failure<T, E> extends Result<T, E> {
  final E error;
  const Failure(this.error);
}

/// 模拟一个安全解析整数的函数,返回 Result 类型
Result<int, String> safeParseInt(String input) {
  try {
    int value = int.parse(input);
    return Success(value);
  } on FormatException {
    return Failure('输入 "$input" 不是有效的整数格式。');
  }
}

void main() async {
  // 1. 合理使用 try-on-catch-finally + 日志记录

  print('--- 尝试读取一个存在的文件 ---');
  File('existing_file.txt').writeAsString('Hello Dart!').then((_) async {
    try {
      String content = await readFileContent('existing_file.txt');
      print('成功读取内容: "$content"');
    } catch (e) {
      print('主函数中捕获到文件读取异常: ${e.runtimeType} - $e');
      // 可以给用户显示一个友好的错误提示
      print('提示用户: 无法加载文件内容,请稍后重试。');
    }
  });

  await Future.delayed(Duration(milliseconds: 100)); // 等待文件操作完成

  print('\n--- 尝试读取一个不存在的文件 ---');
  try {
    String content = await readFileContent('non_existent_file.txt');
    print('成功读取内容: "$content"'); // 不会执行
  } catch (e) {
    print('主函数中捕获到文件读取异常: ${e.runtimeType} - $e');
    print('提示用户: 文件不存在,请检查路径。');
  }

  print('\n--- 尝试读取一个空文件 ---');
  File('empty_file.txt').writeAsString('').then((_) async {
    try {
      String content = await readFileContent('empty_file.txt');
      print('成功读取内容: "$content"'); // 不会执行
    } catch (e) {
      print('主函数中捕获到文件读取异常: ${e.runtimeType} - $e');
      print('提示用户: 文件内容为空。');
    }
  });

  await Future.delayed(Duration(milliseconds: 100)); // 等待文件操作完成

  // 2. Result 类型演示
  print('\n--- 使用 Result 类型进行错误处理 ---');
  var num1Result = safeParseInt('123');
  if (num1Result is Success<int, String>) {
    print('成功解析数字: ${num1Result.value}');
  } else if (num1Result is Failure<int, String>) {
    print('解析数字失败: ${num1Result.error}');
  }

  var num2Result = safeParseInt('hello');
  if (num2Result is Success<int, String>) {
    print('成功解析数字: ${num2Result.value}');
  } else if (num2Result is Failure<int, String>) {
    print('解析数字失败: ${num2Result.error}');
  }

  await Future.delayed(Duration(seconds: 1)); // 等待所有异步操作完成
  print('\n程序执行完毕。');
}

运行结果

--- 尝试读取一个存在的文件 ---
文件读取尝试完成 (existing_file.txt)。
成功读取内容: "Hello Dart!"

--- 尝试读取一个不存在的文件 ---
[LOG] 警告: 文件系统操作失败,路径: non_existent_file.txt - 文件不存在
文件读取尝试完成 (non_existent_file.txt)。
主函数中捕获到文件读取异常: FileSystemException - FileSystemException: FileSystemException: 文件不存在, path = 'non_existent_file.txt'
提示用户: 文件不存在,请检查路径。

--- 尝试读取一个空文件 ---
[LOG] 警告: 文件系统操作失败,路径: empty_file.txt - 文件内容为空
文件读取尝试完成 (empty_file.txt)。
主函数中捕获到文件读取异常: FileSystemException - FileSystemException: FileSystemException: 文件内容为空, path = 'empty_file.txt'
提示用户: 文件内容为空。

--- 使用 Result 类型进行错误处理 ---
成功解析数字: 123
解析数字失败: 输入 "hello" 不是有效的整数格式。

程序执行完毕。

注意事项

  • 过度捕获的危害: 捕获过于宽泛的异常类型(例如直接catch (e)而不加on)可能会隐藏程序中的真正问题,导致难以发现和修复bug。
  • 资源泄漏: 在处理资源(如文件、网络连接)时,务必使用finally块或Future.whenComplete来确保资源被正确释放,即使发生异常。
  • 错误边界: 在大型应用中,考虑在更高级别的组件或服务中建立“错误边界”,集中处理来自其子组件或依赖服务的异常,以防止整个应用程序崩溃。例如,在Flutter中使用ErrorWidget.builderrunZonedGuarded
  • Result类型与异常的选择: Result类型适用于函数可能预期地返回多种状态(成功值或特定错误)的场景,它强制调用者处理所有可能的输出。异常更适用于表示不应该发生的、非预期的错误,或那些会中断正常流程的严重问题。在设计API时,根据错误的可预测性和可恢复性来选择。