DEV Community

Ge Ji
Ge Ji

Posted on

Dart Lesson 20: error handling and logging— making programs more robust

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");
  }
}
Enter fullscreen mode Exit fullscreen mode

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");
  }
}
Enter fullscreen mode Exit fullscreen mode

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");
}
Enter fullscreen mode Exit fullscreen mode
  • 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)";
  }
}
Enter fullscreen mode Exit fullscreen mode

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");
  }
}
Enter fullscreen mode Exit fullscreen mode

Output:

User error: UserException (User ID: 10086): User does not exist
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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");
}
Enter fullscreen mode Exit fullscreen mode

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

  1. Catch errors: Globally catch unhandled exceptions and errors
  2. Collect information: Gather error details, device information, user operation traces, etc.
  3. Send reports: Send error information to backend servers
  4. 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;
}
Enter fullscreen mode Exit fullscreen mode

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'},
    );
  });
}
Enter fullscreen mode Exit fullscreen mode

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");
  }
}
Enter fullscreen mode Exit fullscreen mode

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)