dart学习第 20 节:错误处理与日志 —— 让程序更健壮

在前几节课中,我们学习了元数据与反射,了解了代码自我描述的高级技巧。今天我们将聚焦于让程序更健壮的核心技术 ------错误处理与日志。无论多么完美的程序都可能出现意外,而优秀的错误处理机制能让程序在异常情况下优雅降级,详细的日志则能帮助我们快速定位问题根源。

一、错误与异常:Dart 中的两种错误类型

Dart 中存在两种主要的错误类型:Exception(异常)和 Error(错误),它们都继承自 Object 并实现了 Exception 接口,但适用场景截然不同。

1. Exception(异常):可预见的错误

Exception 表示程序运行过程中可能发生且应该被捕获处理的异常情况,通常是由外部因素或合理的业务逻辑错误导致的。

常见的内置 Exception 类型:

  • FormatException:格式错误(如字符串转数字失败)
  • IOException:IO 操作异常(如文件不存在)
  • TimeoutException:超时异常
  • ArgumentError:参数错误(如传递无效参数)

示例:处理格式异常

dart 复制代码
void main() {
  String numberStr = "123a";

  try {
    int number = int.parse(numberStr);
    print("转换结果:$number");
  } on FormatException {
    print("格式错误:无法将 '$numberStr' 转换为数字");
  } catch (e) {
    // 捕获其他类型的异常
    print("发生未知异常:$e");
  }
}

2. Error(错误):不可恢复的错误

Error 表示程序中的严重问题,通常是由代码逻辑错误导致的,不应该被捕获(捕获后也难以恢复),目的是让程序崩溃并暴露问题。

常见的内置 Error 类型:

  • AssertionError:断言失败(调试阶段检查条件是否成立)
  • NullThrownError:抛出了 null
  • RangeError:索引越界
  • NoSuchMethodError:调用了不存在的方法

示例:索引越界错误(不应该捕获)

dart 复制代码
void main() {
  List<int> numbers = [1, 2, 3];

  try {
    print(numbers[5]); // 索引越界
  } on RangeError catch (e) {
    // 不推荐:这类错误应该在开发阶段修复,而不是捕获
    print("捕获到错误:$e");
  }
}

最佳实践:

  • Exception 用于可预期的异常情况(如网络请求失败),应该捕获并处理
  • Error 用于程序逻辑错误(如空指针、索引越界),应该在开发阶段修复,而非捕获

3. 异常捕获的完整语法

Dart 提供了灵活的异常捕获机制,包含 tryoncatchrethrowfinally

dart 复制代码
import 'dart:io';

void main() {
  try {
    // 可能抛出异常的代码
    riskyOperation();
  } on FormatException catch (e) {
    // 捕获特定类型的异常,并获取异常对象
    print("格式错误:$e");
  } on IOException catch (e, s) {
    // 捕获特定类型的异常,并获取堆栈跟踪(StackTrace)
    print("IO错误:$e");
    print("堆栈信息:$s");
  } catch (e) {
    // 捕获所有异常(相当于 on Exception catch (e))
    print("未知异常:$e");
    rethrow; // 重新抛出异常,让上层处理
  } finally {
    // 无论是否发生异常,都会执行的代码(如资源释放)
    print("操作结束,清理资源");
  }
}

void riskyOperation() {
  throw FormatException("无效的格式");
}
  • on 用于指定捕获的异常类型
  • catch 用于获取异常对象和堆栈跟踪
  • rethrow 用于将异常重新抛出,不破坏原始堆栈信息
  • finally 用于执行必须完成的操作(如关闭文件、释放连接)

二、自定义异常:处理业务特定错误

在实际开发中,系统内置的异常类型往往不足以描述业务场景中的错误(如 "用户不存在""余额不足" 等)。这时我们可以定义自定义异常,使错误处理更精准。

1. 定义自定义异常

自定义异常通常继承自 Exception 类,并包含描述错误的信息:

dart 复制代码
// 用户相关异常
class UserException implements Exception {
  final String message;
  final String? userId; // 可选的用户ID,便于定位问题

  UserException(this.message, {this.userId});

  // 重写toString,便于打印异常信息
  @override
  String toString() {
    return "UserException${userId != null ? ' (用户ID: $userId)' : ''}: $message";
  }
}

// 支付相关异常
class PaymentException implements Exception {
  final String message;
  final double amount; // 涉及的金额

  const PaymentException(this.message, this.amount);
}

2. 抛出与捕获自定义异常

在业务逻辑中抛出自定义异常,并在合适的地方捕获处理:

dart 复制代码
// 模拟用户服务
class UserService {
  // 检查用户是否存在
  void checkUserExists(String userId) {
    // 模拟用户不存在的情况
    if (userId == "10086") {
      throw UserException("用户不存在", userId: userId);
    }
  }
}

// 模拟支付服务
class PaymentService {
  // 处理支付
  void processPayment(String userId, double amount) {
    if (amount <= 0) {
      throw ArgumentError("支付金额必须大于0");
    }

    // 模拟余额不足
    if (amount > 1000) {
      throw PaymentException("余额不足", amount);
    }
  }
}

void main() async {
  final userService = UserService();
  final paymentService = PaymentService();
  const userId = "10086";
  const amount = 1500.0;

  try {
    userService.checkUserExists(userId);
    paymentService.processPayment(userId, amount);
    print("支付成功");
  } on UserException catch (e) {
    // 处理用户相关异常
    print("用户错误:$e");
    // 可以在这里添加日志上报、提示用户等逻辑
  } on PaymentException catch (e) {
    // 处理支付相关异常
    print("支付错误:${e.message},金额:${e.amount}");
  } on ArgumentError catch (e) {
    // 处理参数错误
    print("参数错误:$e");
  } catch (e) {
    // 兜底处理其他异常
    print("发生未知错误:$e");
  }
}

输出结果:

plaintext 复制代码
用户错误:UserException (用户ID: 10086): 用户不存在

自定义异常的优势:

  • 更精准地描述业务错误场景
  • 便于分类处理不同类型的错误
  • 可以携带更多上下文信息(如用户 ID、金额等),助力问题定位

三、日志工具:记录程序运行轨迹

日志是调试和问题排查的重要依据。Dart 内置的 print 函数功能有限,实际开发中推荐使用更强大的日志库,如 logger 包。

1. 使用 logger 包

logger 包提供了分级日志、格式化输出、堆栈跟踪等功能,使用简单且强大。

(1)添加依赖

pubspec.yaml 中添加:

yaml 复制代码
dependencies:
  logger: ^2.6.1

执行 dart pub get 安装。

(2)基本使用
dart 复制代码
import 'package:logger/logger.dart';

// 创建日志实例
final logger = Logger(
  // 日志输出格式
  printer: PrettyPrinter(
    methodCount: 2, // 显示的方法调用栈数量
    errorMethodCount: 5, // 错误日志显示的方法调用栈数量
    lineLength: 120, // 每行长度
    colors: true, // 彩色输出
    printEmojis: true, // 显示 emoji
    printTime: true, // 显示时间
  ),
);

void main() {
  // 不同级别的日志
  logger.v("Verbose 日志:最详细的调试信息");
  logger.d("Debug 日志:调试过程中的信息");
  logger.i("Info 日志:正常的运行信息");
  logger.w("Warning 日志:需要注意的潜在问题");
  logger.e("Error 日志:错误信息");
  logger.wtf("WTF 日志:严重错误,可能导致程序崩溃");

  // 记录异常
  try {
    throw FormatException("无效的格式");
  } catch (e, s) {
    logger.e("解析失败", error: e, stackTrace: s);
  }
}

日志级别从低到高:verbose < debug < info < warning < error < wtf,可以通过配置控制输出哪些级别的日志(如生产环境只输出 info 及以上级别)。

2. 自定义日志输出

logger 包支持自定义日志输出方式(如写入文件、发送到服务器等):

dart 复制代码
import 'package:logger/logger.dart';
import 'dart:io';

// 自定义日志输出:同时打印到控制台和文件
class FileOutput extends LogOutput {
  final File logFile;

  FileOutput(this.logFile);

  @override
  void output(OutputEvent event) {
    // 1. 输出到控制台
    for (var line in event.lines) {
      print(line);
    }

    // 2. 写入文件(异步)
    _writeToFile(event);
  }

  Future<void> _writeToFile(OutputEvent event) async {
    final time = DateTime.now().toIso8601String();
    final logContent = event.lines.map((line) => "[$time] $line\n").join();

    try {
      await logFile.writeAsString(logContent, mode: FileMode.append);
    } catch (e) {
      print("日志写入文件失败:$e");
    }
  }
}

void main() {
  // 创建日志文件
  final logFile = File("app_logs.txt");

  // 配置日志器
  final logger = Logger(
    level: Level.debug, // 输出 debug 及以上级别
    output: FileOutput(logFile), // 使用自定义输出
    printer: SimplePrinter(), // 简单格式
  );

  logger.i("应用启动");
  logger.w("磁盘空间不足");
}

3. 日志最佳实践

  • 分级输出:根据日志重要性使用不同级别,便于过滤
  • 包含上下文:日志中应包含时间、用户 ID、请求 ID 等上下文信息
  • 敏感信息脱敏:避免记录密码、令牌等敏感信息
  • 控制体积:日志文件应定期轮转(删除旧日志),避免占满磁盘
  • 环境区分:开发环境输出详细日志,生产环境只输出关键信息

四、错误上报:线上问题监控

对于线上运行的程序,仅靠本地日志是不够的,需要一套错误上报机制,及时发现并解决用户遇到的问题。

1. 错误上报的基本流程

  1. 捕获错误:全局捕获未处理的异常和错误
  2. 收集信息:收集错误详情、设备信息、用户操作轨迹等
  3. 发送报告:将错误信息发送到后端服务器
  4. 分析处理:开发人员在后台查看错误报告并修复

2. 全局异常捕获

Dart 中可以通过 runZonedGuarded 捕获全局未处理的异常:

dart 复制代码
import 'dart:async';
import 'package:logger/logger.dart';

final logger = Logger();

// 错误上报服务
class ErrorReportingService {
  // 上报错误信息
  static Future<void> reportError(
    dynamic error,
    dynamic stackTrace, {
    Map<String, dynamic>? extraInfo,
  }) async {
    try {
      // 1. 收集错误信息
      final report = {
        'error': error.toString(),
        'stackTrace': stackTrace.toString(),
        'time': DateTime.now().toIso8601String(),
        'extra': extraInfo ?? {},
        // 可以添加设备信息、用户ID等
      };

      // 2. 本地日志记录
      logger.e("错误上报", error: error, stackTrace: stackTrace);

      // 3. 发送到服务器(实际项目中替换为真实接口)
      // await http.post(
      //   Uri.parse('https://your-api.com/errors'),
      //   body: report,
      // );

      print("错误上报成功:$report");
    } catch (e) {
      logger.e("错误上报失败", error: e);
    }
  }
}

void main() {
  // 使用 runZonedGuarded 捕获全局异常
  runZonedGuarded(
    () {
      // 程序入口逻辑
      runApp();
    },
    (error, stackTrace) {
      // 捕获未处理的异常
      ErrorReportingService.reportError(
        error,
        stackTrace,
        extraInfo: {'scene': 'app_start'},
      );
    },
  );
}

void runApp() {
  // 模拟程序运行中发生的错误
  throw UserException("用户会话过期");
}

// 自定义异常
class UserException implements Exception {
  final String message;
  UserException(this.message);
}

3. Flutter 中的错误捕获

在 Flutter 中,除了 Dart 层的异常,还需要处理 Flutter 框架层的错误:

dart 复制代码
import 'dart:async';
import 'package:flutter/material.dart';

void main() {
  // 捕获 Flutter 框架异常
  FlutterError.onError = (details) {
    FlutterError.presentError(details);
    // 上报到错误服务
    ErrorReportingService.reportError(
      details.exception,
      details.stack,
      extraInfo: {'type': 'flutter_framework_error'},
    );
  };

  // 捕获 Dart 层未处理异常
  runZonedGuarded(() => runApp(const MyApp()), (error, stackTrace) {
    ErrorReportingService.reportError(
      error,
      stackTrace,
      extraInfo: {'type': 'dart_uncaught_error'},
    );
  });
}

4. 错误上报的注意事项

  • 用户隐私保护:遵守数据保护法规,不上报敏感信息(如地理位置、通讯录)
  • 批量与压缩:错误报告应压缩后发送,避免频繁请求影响用户体验
  • 离线缓存:网络不佳时缓存错误报告,待网络恢复后发送
  • 避免递归上报:确保错误上报逻辑本身不会抛出异常,导致无限循环

五、综合案例:健壮的网络请求工具

结合错误处理、日志和错误上报,实现一个健壮的网络请求工具:

dart 复制代码
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:logger/logger.dart';

final logger = Logger();

// 自定义异常
class NetworkException implements Exception {
  final String message;
  final int? statusCode; // HTTP状态码
  final String? url;

  NetworkException(this.message, {this.statusCode, this.url});

  @override
  String toString() {
    return "NetworkException (${statusCode ?? 'unknown'}): $message${url != null ? ' (URL: $url)' : ''}";
  }
}

// 网络请求工具
class ApiClient {
  // 发送GET请求
  Future<dynamic> get(String url) async {
    logger.i("发送GET请求", error: {'url': url});

    try {
      final response = await http
          .get(Uri.parse(url))
          .timeout(const Duration(seconds: 10)); // 超时设置

      if (response.statusCode == 200) {
        logger.d("请求成功", error: {'url': url, 'statusCode': 200});
        return json.decode(response.body);
      } else {
        // 非200状态码视为错误
        throw NetworkException(
          "请求失败,状态码:${response.statusCode}",
          statusCode: response.statusCode,
          url: url,
        );
      }
    } on http.ClientException catch (e) {
      // 网络连接错误
      final exception = NetworkException("网络连接错误: ${e.message}", url: url);
      logger.e("网络请求失败", error: exception);
      // 上报错误
      _reportError(exception);
      rethrow;
    } on TimeoutException {
      // 超时错误
      final exception = NetworkException("请求超时", url: url);
      logger.e("网络请求失败", error: exception);
      _reportError(exception);
      rethrow;
    } on FormatException {
      // 解析错误
      final exception = NetworkException("响应格式错误", url: url);
      logger.e("网络请求失败", error: exception);
      _reportError(exception);
      rethrow;
    } catch (e) {
      // 其他错误
      final exception = NetworkException("未知错误: $e", url: url);
      logger.e("网络请求失败", error: exception);
      _reportError(exception);
      rethrow;
    }
  }

  // 错误上报
  void _reportError(NetworkException e) {
    // 实际项目中实现错误上报逻辑
    logger.w("错误将被上报: $e");
  }
}

// 使用示例
void main() async {
  final apiClient = ApiClient();

  try {
    final data = await apiClient.get("https://api.example.com/data");
    print("获取数据成功: $data");
  } on NetworkException catch (e) {
    // 处理网络异常(如提示用户检查网络)
    print("请求失败: $e");
  }
}

这个网络请求工具具备以下特性:

  • 捕获各种可能的网络错误(连接失败、超时、解析错误等)
  • 使用自定义异常 NetworkException 统一错误类型
  • 详细的日志记录(请求开始、成功、失败)
  • 错误上报机制
  • 对用户友好的错误提示
相关推荐
傅里叶4 小时前
Flutter项目使用 buf.build
flutter
恋猫de小郭6 小时前
iOS 26 开始强制 UIScene ,你的 Flutter 插件准备好迁移支持了吗?
android·前端·flutter
yuanlaile6 小时前
Flutter开发HarmonyOS鸿蒙App商业项目实战已出炉
flutter·华为·harmonyos
CodeCaptain7 小时前
可直接落地的「Flutter 桥接鸿蒙 WebSocket」端到端实施方案
websocket·flutter·harmonyos
stringwu7 小时前
Flutter 中的 MVVM 架构实现指南
前端·flutter
消失的旧时光-194320 小时前
Flutter 异步体系终章:FutureBuilder 与 StreamBuilder 架构优化指南
flutter·架构
消失的旧时光-19431 天前
Flutter 异步 + 状态管理融合实践:Riverpod 与 Bloc 双方案解析
flutter
程序员老刘1 天前
Flutter版本选择指南:避坑3.27,3.35基本稳定 | 2025年10月
flutter·客户端
—Qeyser1 天前
Flutter网络请求Dio封装实战
网络·flutter·php·xcode·android-studio