In previous lessons, we explored metadata and reflection, learning advanced techniques for code self-description. Today we'll focus on core technologies that make programs more robust — error handling and logging. No matter how perfect a program is, unexpected issues can arise. Excellent error handling allows programs to gracefully degrade in exceptional situations, while detailed logging helps us quickly identify the root cause of problems.
I. Errors and Exceptions: Two Types of Errors in Dart
Dart has two main types of errors: Exception and Error. Both inherit from Object and implement the Exception interface but serve completely different purposes.
1. Exception: Foreseeable Errors
Exception represents potential, catchable issues during program execution, typically caused by external factors or reasonable business logic errors.
Common built-in Exception types:
- FormatException: Format errors (e.g., failed string-to-number conversion)
- IOException: IO operation exceptions (e.g., file not found)
- TimeoutException: Timeout exceptions
- ArgumentError: Invalid parameter errors
Example: Handling format exceptions
void main() {
String numberStr = "123a";
try {
int number = int.parse(numberStr);
print("Conversion result: $number");
} on FormatException {
print("Format error: Cannot convert '$numberStr' to a number");
} catch (e) {
// Catch other types of exceptions
print("Unknown exception occurred: $e");
}
}
2. Error: Unrecoverable Issues
Error represents serious problems in a program, typically caused by logical code errors. They shouldn't be caught (as recovery is difficult) and are intended to crash the program to expose issues.
Common built-in Error types:
- AssertionError: Assertion failure (checking conditions during debugging)
- NullThrownError: Null was thrown
- RangeError: Index out of bounds
- NoSuchMethodError: Calling a non-existent method
Example: Index out-of-bounds error (should not be caught)
void main() {
List<int> numbers = [1, 2, 3];
try {
print(numbers[5]); // Index out of bounds
} on RangeError catch (e) {
// Not recommended: These errors should be fixed during development, not caught
print("Caught error: $e");
}
}
Best Practice:
- Use Exception for expected exceptional situations (e.g., failed network requests) that should be caught and handled
- Use Error for program logic errors (e.g., null pointers, index out of bounds) that should be fixed during development, not caught
3. Complete Exception Handling Syntax
Dart provides a flexible exception handling mechanism with try, on, catch, rethrow, and finally:
import 'dart:io';
void main() {
try {
// Code that might throw an exception
riskyOperation();
} on FormatException catch (e) {
// Catch specific type of exception and get the exception object
print("Format error: $e");
} on IOException catch (e, s) {
// Catch specific type and get stack trace
print("IO error: $e");
print("Stack trace: $s");
} catch (e) {
// Catch all exceptions (equivalent to on Exception catch (e))
print("Unknown exception: $e");
rethrow; // Re-throw the exception for upper-level handling
} finally {
// Code that executes regardless of exceptions (e.g., resource release)
print("Operation completed, cleaning up resources");
}
}
void riskyOperation() {
throw FormatException("Invalid format");
}
- on specifies the exception type to catch
- catch captures the exception object and stack trace
- rethrow re-throws the exception without breaking the original stack trace
- finally executes code that must complete (e.g., closing files, releasing connections)
II. Custom Exceptions: Handling Business-Specific Errors
In practical development, built-in exception types are often insufficient for describing business-specific errors (like "user not found" or "insufficient funds"). We can define custom exceptions for more precise error handling.
1. Defining Custom Exceptions
Custom exceptions typically implement the Exception interface and contain error-describing information:
// User-related exceptions
class UserException implements Exception {
final String message;
final String? userId; // Optional user ID for problem identification
const UserException(this.message, {this.userId});
// Override toString for better error messages
@override
String toString() {
return "UserException${userId != null ? ' (User ID: $userId)' : ''}: $message";
}
}
// Payment-related exceptions
class PaymentException implements Exception {
final String message;
final double amount; // Related amount
const PaymentException(this.message, this.amount);
@override
String toString() {
return "PaymentException: $message (Amount: $amount)";
}
}
2. Throwing and Catching Custom Exceptions
Throw custom exceptions in business logic and catch them at appropriate points:
// Simulated user service
class UserService {
// Check if user exists
void checkUserExists(String userId) {
// Simulate user not found scenario
if (userId == "10086") {
throw UserException("User does not exist", userId: userId);
}
}
}
// Simulated payment service
class PaymentService {
// Process payment
void processPayment(String userId, double amount) {
if (amount <= 0) {
throw ArgumentError("Payment amount must be greater than 0");
}
// Simulate insufficient funds
if (amount > 1000) {
throw PaymentException("Insufficient funds", 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("Payment successful");
} on UserException catch (e) {
// Handle user-related exceptions
print("User error: $e");
// Add logging, user notifications, etc. here
} on PaymentException catch (e) {
// Handle payment-related exceptions
print("Payment error: ${e.message}, Amount: ${e.amount}");
} on ArgumentError catch (e) {
// Handle parameter errors
print("Parameter error: $e");
} catch (e) {
// Fallback for other exceptions
print("Unknown error occurred: $e");
}
}
Output:
User error: UserException (User ID: 10086): User does not exist
Advantages of custom exceptions:
- More accurately describe business error scenarios
- Facilitate categorized handling of different error types
- Can carry additional context information (like user ID, amount) to aid problem identification
III. Logging Tools: Recording Program Execution Traces
Logs are crucial for debugging and troubleshooting. Dart's built-in print function is limited, so using more powerful logging libraries like the logger package is recommended for actual development.
1. Using the logger Package
The logger package provides leveled logging, formatted output, stack traces, and more, with simple and powerful usage.
(1) Add Dependency
Add to pubspec.yaml:
dependencies:
logger: ^2.6.1
Run dart pub get to install.
(2) Basic Usage
import 'package:logger/logger.dart';
// Create logger instance
final logger = Logger(
// Log output format
printer: PrettyPrinter(
methodCount: 2, // Number of method calls to display
errorMethodCount: 5, // Number of method calls for error logs
lineLength: 120, // Line length
colors: true, // Colored output
printEmojis: true, // Show emojis
printTime: true, // Show time
),
);
void main() {
// Different log levels
logger.v("Verbose log: Most detailed debugging information");
logger.d("Debug log: Information during debugging");
logger.i("Info log: Normal operation information");
logger.w("Warning log: Potential issues to note");
logger.e("Error log: Error information");
logger.wtf("WTF log: Severe error, may cause program crash");
// Log exceptions
try {
throw FormatException("Invalid format");
} catch (e, s) {
logger.e("Parsing failed", error: e, stackTrace: s);
}
}
Log levels from lowest to highest: verbose < debug < info < warning < error < wtf. You can configure which levels to output (e.g., only info and above in production).
2. Custom Log Output
The logger package supports custom log output methods (like writing to files or sending to servers):
import 'package:logger/logger.dart';
import 'dart:io';
// Custom log output: print to console and file simultaneously
class FileOutput extends LogOutput {
final File logFile;
FileOutput(this.logFile);
@override
void output(OutputEvent event) {
// 1. Output to console
for (var line in event.lines) {
print(line);
}
// 2. Write to file (asynchronously)
_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("Failed to write log to file: $e");
}
}
}
void main() {
// Create log file
final logFile = File("app_logs.txt");
// Configure logger
final logger = Logger(
level: Level.debug, // Output debug and above levels
output: FileOutput(logFile), // Use custom output
printer: SimplePrinter(), // Simple format
);
logger.i("Application started");
logger.w("Low disk space");
}
3. Logging Best Practices
- Leveled output: Use different levels based on log importance for easier filtering
- Include context: Logs should contain context like time, user ID, request ID
- Sensitive information masking: Avoid logging passwords, tokens, and other sensitive data
- Size control: Log files should be rotated regularly (delete old logs) to avoid disk overflow
- Environment differentiation: Output detailed logs in development, only critical information in production
IV. Error Reporting: Monitoring Production Issues
For production-running programs, local logs alone are insufficient. An error reporting mechanism is needed to promptly identify and resolve issues encountered by users.
1. Basic Error Reporting Flow
- Catch errors: Globally catch unhandled exceptions and errors
- Collect information: Gather error details, device information, user operation traces, etc.
- Send reports: Send error information to backend servers
- Analyze and handle: Developers review error reports and fix issues
2. Global Exception Catching
In Dart, you can catch globally unhandled exceptions with runZonedGuarded:
import 'dart:async';
import 'package:logger/logger.dart';
final logger = Logger();
// Error reporting service
class ErrorReportingService {
// Report error information
static Future<void> reportError(
dynamic error,
dynamic stackTrace, {
Map<String, dynamic>? extraInfo,
}) async {
try {
// 1. Collect error information
final report = {
'error': error.toString(),
'stackTrace': stackTrace.toString(),
'time': DateTime.now().toIso8601String(),
'extra': extraInfo ?? {},
// Add device information, user ID, etc.
};
// 2. Local log recording
logger.e("Error reported", error: error, stackTrace: stackTrace);
// 3. Send to server (replace with real API in actual project)
// await http.post(
// Uri.parse('https://your-api.com/errors'),
// body: report,
// );
print("Error reported successfully: $report");
} catch (e) {
logger.e("Failed to report error", error: e);
}
}
}
void main() {
// Use runZonedGuarded to catch global exceptions
runZonedGuarded(
() {
// Program entry logic
runApp();
},
(error, stackTrace) {
// Catch unhandled exceptions
ErrorReportingService.reportError(
error,
stackTrace,
extraInfo: {'scene': 'app_start'},
);
},
);
}
void runApp() {
// Simulate an error occurring during program execution
throw UserException("User session expired");
}
// Custom exception
class UserException implements Exception {
final String message;
UserException(this.message);
@override
String toString() => message;
}
3. Error Catching in Flutter
In Flutter, besides Dart layer exceptions, you need to handle Flutter framework layer errors:
import 'dart:async';
import 'package:flutter/material.dart';
void main() {
// Catch Flutter framework exceptions
FlutterError.onError = (details) {
FlutterError.presentError(details);
// Report to error service
ErrorReportingService.reportError(
details.exception,
details.stack,
extraInfo: {'type': 'flutter_framework_error'},
);
};
// Catch Dart layer unhandled exceptions
runZonedGuarded(() => runApp(const MyApp()), (error, stackTrace) {
ErrorReportingService.reportError(
error,
stackTrace,
extraInfo: {'type': 'dart_uncaught_error'},
);
});
}
4. Error Reporting Considerations
- User privacy protection: Comply with data protection regulations; don't report sensitive information (like location, contacts)
- Batching and compression: Error reports should be compressed before sending to avoid frequent requests affecting user experience
- Offline caching: Cache error reports when network is poor, send when connection is restored
- Avoid recursive reporting: Ensure error reporting logic doesn't throw exceptions, causing infinite loops
V. Comprehensive Case: Robust Network Request Tool
Combine error handling, logging, and error reporting to implement a robust network request tool:
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:logger/logger.dart';
final logger = Logger();
// Custom exception
class NetworkException implements Exception {
final String message;
final int? statusCode; // HTTP status code
final String? url;
NetworkException(this.message, {this.statusCode, this.url});
@override
String toString() {
return "NetworkException (${statusCode ?? 'unknown'}): $message${url != null ? ' (URL: $url)' : ''}";
}
}
// Network request tool
class ApiClient {
// Send GET request
Future<dynamic> get(String url) async {
logger.i("Sending GET request", error: {'url': url});
try {
final response = await http
.get(Uri.parse(url))
.timeout(const Duration(seconds: 10)); // Timeout setting
if (response.statusCode == 200) {
logger.d("Request successful", error: {'url': url, 'statusCode': 200});
return json.decode(response.body);
} else {
// Non-200 status codes are considered errors
throw NetworkException(
"Request failed with status code: ${response.statusCode}",
statusCode: response.statusCode,
url: url,
);
}
} on http.ClientException catch (e) {
// Network connection error
final exception = NetworkException(
"Network connection error: ${e.message}",
url: url,
);
logger.e("Network request failed", error: exception);
// Report error
_reportError(exception);
rethrow;
} on TimeoutException {
// Timeout error
final exception = NetworkException("Request timed out", url: url);
logger.e("Network request failed", error: exception);
_reportError(exception);
rethrow;
} on FormatException {
// Parsing error
final exception = NetworkException("Response format error", url: url);
logger.e("Network request failed", error: exception);
_reportError(exception);
rethrow;
} catch (e) {
// Other errors
final exception = NetworkException("Unknown error: $e", url: url);
logger.e("Network request failed", error: exception);
_reportError(exception);
rethrow;
}
}
// Error reporting
void _reportError(NetworkException e) {
// Implement error reporting logic in actual projects
logger.w("Error will be reported: $e");
}
}
// Usage example
void main() async {
final apiClient = ApiClient();
try {
final data = await apiClient.get("https://api.example.com/data");
print("Data fetched successfully: $data");
} on NetworkException catch (e) {
// Handle network exceptions (e.g., prompt user to check network)
print("Request failed: $e");
}
}
This network request tool has the following features:
- Catches various potential network errors (connection failures, timeouts, parsing errors, etc.)
- Uses custom NetworkException for unified error type
- Detailed logging (request start, success, failure)
- Error reporting mechanism
- User-friendly error prompts
Top comments (0)