在前几节课中,我们学习了元数据与反射,了解了代码自我描述的高级技巧。今天我们将聚焦于让程序更健壮的核心技术 ------错误处理与日志。无论多么完美的程序都可能出现意外,而优秀的错误处理机制能让程序在异常情况下优雅降级,详细的日志则能帮助我们快速定位问题根源。
一、错误与异常: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
:抛出了 nullRangeError
:索引越界NoSuchMethodError
:调用了不存在的方法
示例:索引越界错误(不应该捕获)
dart
void main() {
List<int> numbers = [1, 2, 3];
try {
print(numbers[5]); // 索引越界
} on RangeError catch (e) {
// 不推荐:这类错误应该在开发阶段修复,而不是捕获
print("捕获到错误:$e");
}
}
最佳实践:
Exception
用于可预期的异常情况(如网络请求失败),应该捕获并处理Error
用于程序逻辑错误(如空指针、索引越界),应该在开发阶段修复,而非捕获
3. 异常捕获的完整语法
Dart 提供了灵活的异常捕获机制,包含 try
、on
、catch
、rethrow
和 finally
:
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. 错误上报的基本流程
- 捕获错误:全局捕获未处理的异常和错误
- 收集信息:收集错误详情、设备信息、用户操作轨迹等
- 发送报告:将错误信息发送到后端服务器
- 分析处理:开发人员在后台查看错误报告并修复
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
统一错误类型 - 详细的日志记录(请求开始、成功、失败)
- 错误上报机制
- 对用户友好的错误提示