现在位置: 首页 > Dart 教程 > 正文

Dart 异常处理

异常是程序运行时发生的错误事件。

良好的异常处理能让程序在遇到错误时优雅地降级,而不是直接崩溃。

本章介绍 Dart 中的 try/catch/finally、throw 抛出异常、on 捕获指定类型以及 rethrow 重新抛出。


try / catch / finally 基础

try 块中放置可能抛出异常的代码,catch 块捕获并处理异常,finally 块无论是否异常都会执行。

实例

void main() {
  // 基本的 try-catch
  try {
    int result = 10 ~/ 0;  // 除零操作会抛出异常
    print('结果: $result');  // 这行不会执行
  } catch (e) {
    // e 是异常对象
    print('捕获到异常: $e');
  }

  // 获取异常和堆栈信息
  try {
    List<int> numbers = [1, 2, 3];
    print(numbers[10]);  // 访问越界索引
  } catch (e, stackTrace) {
    // e 是异常,stackTrace 是调用堆栈
    print('RUNOOB 异常: $e');
    print('堆栈信息:\n$stackTrace');
  }

  // finally:无论是否异常都会执行
  try {
    print('尝试执行操作...');
    int result = 5 ~/ 0;
    print('这行不会执行: $result');
  } catch (e) {
    print('出错: $e');
  } finally {
    // finally 块中的代码始终执行,常用于清理资源
    print('清理资源完成(finally 总是执行)');
  }
}
捕获到异常: IntegerDivisionByZeroException
RUNOOB 异常: RangeError (index): Invalid value: Not in inclusive range 0..2: 10
堆栈信息:
#0      List.[] (dart:core-patch/growable_array.dart:264)
#1      main (file:///...)
...
尝试执行操作...
出错: IntegerDivisionByZeroException
清理资源完成(finally 总是执行)

finally 块最适合用于资源清理,比如关闭文件、释放数据库连接等。即使 try 或 catch 中有 return 语句,finally 仍然会执行。


throw 抛出异常

使用 throw 关键字可以主动抛出异常。

Dart 允许抛出任何对象作为异常(不仅仅是 Exception 的子类),但最佳实践是抛出 Exception 或 Error 的子类。

实例

// 自定义异常类
class InvalidAgeException implements Exception {
  final int age;
  final String message;

  InvalidAgeException(this.age)
      : message = '无效的年龄: $age(年龄必须在 0 到 150 之间)';

  @override
  String toString() => 'InvalidAgeException: $message';
}

// 使用自定义异常的函数
void validateAge(int age) {
  if (age < 0) {
    throw InvalidAgeException(age);
  }
  if (age > 150) {
    throw InvalidAgeException(age);
  }
  print('年龄 $age 验证通过');
}

// 抛出内置异常
int divide(int a, int b) {
  if (b == 0) {
    throw ArgumentError('除数不能为 0');  // 使用内置异常
  }
  return a ~/ b;
}

void main() {
  // 测试自定义异常
  try {
    validateAge(-5);
  } catch (e) {
    print(e);
  }

  try {
    validateAge(200);
  } catch (e) {
    print(e);
  }

  validateAge(25);  // 正常情况

  // 测试抛出内置异常
  try {
    divide(10, 0);
  } catch (e) {
    print('RUNOOB 错误: $e');
  }
}
InvalidAgeException: 无效的年龄: -5(年龄必须在 0 到 150 之间)
InvalidAgeException: 无效的年龄: 200(年龄必须在 0 到 150 之间)
年龄 25 验证通过
RUNOOB 错误: Invalid argument(s): 除数不能为 0

Exception 和 Error 的区别:Exception 是可预期的、程序可以处理的错误(如网络超时、文件不存在);Error 是不可预期的、通常表示程序 bug 的问题(如类型错误、空指针)。你的代码应该捕获 Exception 而非 Error。


on 捕获指定类型

on 关键字可以按异常类型进行捕获,让错误处理更精细。

实例

void main() {
  // 按类型捕获不同的异常
  try {
    // 模拟不同类型的异常
    int result = performOperation('divide_by_zero');
    print('结果: $result');
  } on IntegerDivisionByZeroException {
    // 只捕获除零异常
    print('RUNOOB 错误: 不能除以零');
  } on FormatException catch (e) {
    // 捕获格式异常,同时获取异常信息
    print('RUNOOB 格式错误: ${e.message}');
  } on RangeError {
    // 只捕获范围错误
    print('RUNOOB 错误: 索引越界');
  } catch (e) {
    // 兜底:捕获所有其他异常
    print('未知错误: $e');
  } finally {
    print('操作结束\n');
  }

  // 按顺序匹配的示例
  print('--- 测试不同的操作 ---');
  for (var op in ['divide_by_zero', 'parse_error', 'out_of_range', 'normal']) {
    try {
      performOperation(op);
    } on IntegerDivisionByZeroException {
      print('$op -> 除零异常');
    } on FormatException {
      print('$op -> 格式异常');
    } on RangeError {
      print('$op -> 范围异常');
    } catch (e) {
      print('$op -> 其他异常: $e');
    }
  }
}

int performOperation(String operation) {
  switch (operation) {
    case 'divide_by_zero':
      return 10 ~/ 0;  // 抛出 IntegerDivisionByZeroException
    case 'parse_error':
      int.parse('not_a_number');  // 抛出 FormatException
      return 0;
    case 'out_of_range':
      var list = [1, 2, 3];
      return list[100];  // 抛出 RangeError
    case 'normal':
      return 42;
    default:
      throw Exception('未知操作');
  }
}
RUNOOB 错误: 不能除以零
操作结束

--- 测试不同的操作 ---
divide_by_zero -> 除零异常
parse_error -> 格式异常
out_of_range -> 范围异常

on 和 catch 的排列顺序很重要:Dart 会按照从上到下的顺序匹配异常类型。更具体的异常类型应该放在前面,更通用的放在后面。如果把 catch (e) 放在最前面,后面的 on 就永远不会被执行。


rethrow 重新抛出

有时你需要在捕获异常后做一些处理(如记录日志),然后让异常继续向上传播。

rethrow 关键字用于重新抛出当前捕获的异常,保留原始堆栈信息。

实例

// 模拟一个多层调用的场景
void databaseOperation() {
  print('  正在连接数据库...');
  throw Exception('RUNOOB 数据库连接失败');
}

void serviceLayer() {
  try {
    print('  服务层: 调用数据库操作');
    databaseOperation();
  } catch (e) {
    // 先记录日志,然后重新抛出
    print('  [日志] 数据库操作异常: $e');
    rethrow;  // 保留原始异常和堆栈,继续向上传播
  }
}

void controllerLayer() {
  try {
    print('控制器层: 处理请求');
    serviceLayer();
  } catch (e, stackTrace) {
    // 最外层捕获并处理
    print('控制器层捕获到异常');
    print('错误信息: $e');
    print('给用户返回: "服务暂时不可用,请稍后重试"');
  }
}

void main() {
  controllerLayer();
  print('\n--- rethrow vs throw e 的区别 ---');

  // 演示 rethrow 和 throw e 的区别
  try {
    try {
      throw FormatException('原始异常');
    } catch (e) {
      // throw e:会重置堆栈信息,丢失原始调用位置
      print('使用 throw e(丢失原始堆栈)...');
      throw e;  // 堆栈从这里开始
    }
  } catch (e, stack) {
    print('捕获: $e');
    print('堆栈指向这里(不是原始位置):\n$stack');
  }
}
控制器层: 处理请求
  服务层: 调用数据库操作
  正在连接数据库...
  [日志] 数据库操作异常: Exception: RUNOOB 数据库连接失败
控制器层捕获到异常
错误信息: Exception: RUNOOB 数据库连接失败
给用户返回: "服务暂时不可用,请稍后重试"

--- rethrow vs throw e 的区别 ---
使用 throw e(丢失原始堆栈)...
捕获: FormatException: 原始异常
堆栈指向这里(不是原始位置):
...

rethrow 和 throw e 的区别很关键:rethrow 保留原始异常的全部堆栈信息,方便定位问题根源;throw e 会重置堆栈,从当前位置重新开始。在记录日志后需要继续传播异常时,请使用 rethrow。


异常处理的最佳实践

实践说明
只捕获能处理的异常不要为了"不报错"而盲目 catch 所有异常。如果不知道如何处理,让它向上传播
按类型捕获使用 on 指定异常类型,避免用一个 catch 吞掉所有异常
finally 清理资源文件、网络连接等资源在 finally 中关闭
不要吞掉异常捕获了异常但什么都不做(空 catch 块)是危险的,至少记录日志
使用 rethrow需要记录日志后继续传播异常时,用 rethrow 而非 throw e
异常消息要具体throw Exception('数据库连接失败: 192.168.1.1:3306') 比 throw Exception('出错了') 更有用