跳转至正文

使用 Result 对象进行错误处理

使用 Result 对象改善跨类的错误处理。

Dart 提供内置错误处理机制,支持抛出与捕获异常。

错误处理文档 所述,Dart 的异常是未处理异常 (unhandled exceptions)。这意味着抛出异常的方法无需声明异常,调用方也不必捕获。

这可能导致异常未得到妥善处理。在大型项目中,开发者可能忘记捕获异常,各层与组件可能抛出未文档化的异常,进而导致错误与崩溃。

本指南将介绍这一局限,以及如何用 结果类型 (result) 模式缓解。

Flutter 应用中的错误流

#

遵循 Flutter 架构指南 的应用通常由 view model、repository、service 等组成。当其中某组件的函数失败时,应将错误告知调用方。

通常通过异常完成。例如,无法与远程服务器通信的 API 客户端 service 可能抛出 HTTP 错误异常;调用方(如 Repository)须捕获该异常,或忽略并由 view model 处理。

以下示例可见这一点。考虑这些类:

  • Service ApiClientService 向远程服务发起 API 调用。

  • Repository UserProfileRepository 提供由 ApiClientService 获取的 UserProfile

  • View model UserProfileViewModel 使用 UserProfileRepository

ApiClientServicegetUserProfile 在特定情况下会抛出异常:

  • 响应码非 200 时抛出 HttpException

  • 响应格式不正确时 JSON 解析抛出异常。

  • HTTP 客户端可能因网络问题抛出异常。

以下代码处理多种可能的异常:

dart
class ApiClientService {
  // ···

  Future<UserProfile> getUserProfile() async {
    try {
      final request = await client.get(_host, _port, '/user');
      final response = await request.close();
      if (response.statusCode == 200) {
        final stringData = await response.transform(utf8.decoder).join();
        return UserProfile.fromJson(jsonDecode(stringData));
      } else {
        throw const HttpException('Invalid response');
      }
    } finally {
      client.close();
    }
  }
}

UserProfileRepository 无需处理 ApiClientService 的异常;本例中它直接返回 API 客户端的值。

dart
class UserProfileRepository {
  // ···

  Future<UserProfile> getUserProfile() async {
    return await _apiClientService.getUserProfile();
  }
}

最后,UserProfileViewModel 应捕获所有异常并处理错误。

可用 try-catch 包装对 UserProfileRepository 的调用:

dart
class UserProfileViewModel extends ChangeNotifier {
  // ···

  Future<void> load() async {
    try {
      _userProfile = await userProfileRepository.getUserProfile();
      notifyListeners();
    } on Exception catch (exception) {
      // handle exception
    }
  }
}

现实中开发者可能忘记正确捕获异常,写出如下代码。它能编译运行,但若前述任一异常发生则会崩溃:

dart
class UserProfileViewModel extends ChangeNotifier {
  // ···

  Future<void> load() async {
    _userProfile = await userProfileRepository.getUserProfile();
    notifyListeners();
  }
}

可尝试为 ApiClientService 文档化可能抛出的异常。但 view model 不直接使用 service,其他开发者可能忽略该信息。

使用结果类型模式

#

抛异常的替代方案是将函数输出包装在 Result 对象中。

成功时 Result 含返回值;失败时含错误。

Resultsealed 类,子类为 OkError;成功值用 Ok 返回,捕获的错误用 Error 返回。

以下是为演示简化的 Result 示例,完整实现见文末。

dart
/// Utility class that simplifies handling errors.
///
/// Return a [Result] from a function to indicate success or failure.
///
/// A [Result] is either an [Ok] with a value of type [T]
/// or an [Error] with an [Exception].
///
/// Use [Result.ok] to create a successful result with a value of type [T].
/// Use [Result.error] to create an error result with an [Exception].
sealed class Result<T> {
  const Result();

  /// Creates an instance of Result containing a value
  factory Result.ok(T value) => Ok(value);

  /// Create an instance of Result containing an error
  factory Result.error(Exception error) => Error(error);
}

/// Subclass of Result for values
final class Ok<T> extends Result<T> {
  const Ok(this.value);

  /// Returned value in result
  final T value;
}

/// Subclass of Result for errors
final class Error<T> extends Result<T> {
  const Error(this.error);

  /// Returned error in result
  final Exception error;
}

本例中 Result 用泛型 T 表示任意返回值,可为 StringintUserProfile 等。

创建 Result 对象

#

使用 Result 返回值的函数不再直接返回值,而是返回包含值的 Result

例如 ApiClientServicegetUserProfile 改为返回 Result

dart
class ApiClientService {
  // ···

  Future<Result<UserProfile>> getUserProfile() async {
    // ···
  }
}

不再直接返回 UserProfile,而是返回包含 UserProfileResult

Result 提供 Result.okResult.error 命名构造函数,按输出构造 Result,并捕获代码抛出的异常包装进 Result

例如 getUserProfile() 已改为使用 Result

dart
class ApiClientService {
  // ···

  Future<Result<UserProfile>> getUserProfile() async {
    try {
      final request = await client.get(_host, _port, '/user');
      final response = await request.close();
      if (response.statusCode == 200) {
        final stringData = await response.transform(utf8.decoder).join();
        return Result.ok(UserProfile.fromJson(jsonDecode(stringData)));
      } else {
        return const Result.error(HttpException('Invalid response'));
      }
    } on Exception catch (exception) {
      return Result.error(exception);
    } finally {
      client.close();
    }
  }
}

原 return 改为 Result.ok 返回;throw HttpException() 改为 Result.error(HttpException());并用 try-catch 将 HTTP 客户端或 JSON 解析器抛出的异常捕获为 Result.error

Repository 类也需修改,直接返回 UserProfile 改为返回 Result<UserProfile>

dart
Future<Result<UserProfile>> getUserProfile() async {
  return await _apiClientService.getUserProfile();
}

解包 Result 对象

#

现在 view model 收到的是包含 UserProfileResult,而非直接的 UserProfile

这迫使实现 view model 的开发者解包 Result 获取 UserProfile,避免未捕获异常。

dart
class UserProfileViewModel extends ChangeNotifier {
  // ···

  UserProfile? userProfile;

  Exception? error;

  Future<void> load() async {
    final result = await userProfileRepository.getUserProfile();
    switch (result) {
      case Ok<UserProfile>():
        userProfile = result.value;
      case Error<UserProfile>():
        error = result.error;
    }
    notifyListeners();
  }
}

Resultsealed 实现,只能是 OkError,可用 switch 结果或表达式 求值。

Ok<UserProfile> 时用 value 属性获取值。

Error<UserProfile> 时用 error 属性获取错误对象。

改善控制流

#

try-catch 确保抛出的异常被捕获而不传播到其他代码。

考虑以下代码。

dart
class UserProfileRepository {
  // ···

  Future<UserProfile> getUserProfile() async {
    try {
      return await _apiClientService.getUserProfile();
    } catch (e) {
      try {
        return await _databaseService.createTemporaryUser();
      } catch (e) {
        throw Exception('Failed to get user profile');
      }
    }
  }
}

此方法中 UserProfileRepository 先通过 ApiClientService 获取 UserProfile,失败则尝试在 DatabaseService 创建临时用户。

因两种 service 方法都可能失败,代码须在两种情况下捕获异常。

可用 Result 模式改进:

dart
Future<Result<UserProfile>> getUserProfile() async {
  final apiResult = await _apiClientService.getUserProfile();
  if (apiResult is Ok) {
    return apiResult;
  }

  final databaseResult = await _databaseService.createTemporaryUser();
  if (databaseResult is Ok) {
    return databaseResult;
  }

  return Result.error(Exception('Failed to get user profile'));
}

ResultOk 则返回该对象,否则返回 Result.error

总结

#

本指南介绍了如何使用 Result 类返回结果值。

要点:

  • Result 类迫使调用方检查错误,减少未捕获异常导致的 bug。

  • 相比 try-catch,Result 类有助于改善控制流。

  • Result 类为 sealed,只能为 OkError,可用 switch 解包。

下文为 Flutter 架构指南Compass 应用示例 中的完整 Result 类。

dart
/// Utility class that simplifies handling errors.
///
/// Return a [Result] from a function to indicate success or failure.
///
/// A [Result] is either an [Ok] with a value of type [T]
/// or an [Error] with an [Exception].
///
/// Use [Result.ok] to create a successful result with a value of type [T].
/// Use [Result.error] to create an error result with an [Exception].
///
/// Evaluate the result using a switch statement:
/// ```dart
/// switch (result) {
///   case Ok(): {
///     print(result.value);
///   }
///   case Error(): {
///     print(result.error);
///   }
/// }
/// ```
sealed class Result<T> {
  const Result();

  /// Creates a successful [Result], completed with the specified [value].
  const factory Result.ok(T value) = Ok._;

  /// Creates an error [Result], completed with the specified [error].
  const factory Result.error(Exception error) = Error._;
}

/// A successful [Result] with a returned [value].
final class Ok<T> extends Result<T> {
  const Ok._(this.value);

  /// The returned value of this result.
  final T value;

  @override
  String toString() => 'Result<$T>.ok($value)';
}

/// An error [Result] with a resulting [error].
final class Error<T> extends Result<T> {
  const Error._(this.error);

  /// The resulting error of this result.
  final Exception error;

  @override
  String toString() => 'Result<$T>.error($error)';
}