-
Notifications
You must be signed in to change notification settings - Fork 3
Error Handling
The Result pattern is a design pattern used to represent the outcome of an operation in a way that explicitly handles success and failure scenarios.
In the provided code, the Result pattern is implemented using a sealed class hierarchy:
sealed class Result<T> {
const Result({required this.isSuccess});
final bool isSuccess;
}-
Resultis an abstract class with a single boolean propertyisSuccess. This property indicates whether the operation represented by the result was successful (true) or not (false). -
Resultis marked assealed, meaning it cannot be extended outside of its library. This allows us to use exhaustive pattern-matching usingswitch-casestatements when handling the results.
Two concrete subclasses of Result are defined: Success and Failure.
final class Success<T> extends Result<T> {
const Success(this.value) : super(isSuccess: true);
final T value;
}-
Successrepresents a successful outcome and contains a value of typeT. It extendsResultand setsisSuccesstotrue.
final class Failure<T> extends Result<T> {
const Failure(this.exception, [this.stackTrace]) : super(isSuccess: false);
final Exception exception;
final StackTrace? stackTrace;
}-
Failurerepresents a failure and contains an exception (and an optional stack trace) indicating why the operation failed. It extendsResultand setsisSuccesstofalse.
For example, the following function returns a Result instance:
Future<Result<PostEntity>> execute(int id) async {
try {
final PostEntity post = await _postRepository.getPost(id);
return Success<PostEntity>(post);
} catch (e, st) {
return Failure<PostEntity>(Exception(e), st);
}
}- If the operation succeeds, the function returns a
Successinstance containing the post. - If the operation fails, the function returns a
Failureinstance containing the exception and stack trace.
The caller can then handle the result as follows:
final Result<PostEntity> getPostResult = await _postQueryService.getPost(postId);
switch (getPostResult) {
case Success<PostEntity>():
_post.value = getPostResult.value;
case Failure<PostEntity>(exception: final SomeException exception):
// Handle `SomeException` here.
_logger.log(LogLevel.error, 'Failed to get post', exception);
case Failure<PostEntity>():
// It is implied that the exception is `Exception`.
_logger.log(LogLevel.error, 'Failed to get post', getPostResult.exception, getPostResult.stackTrace);
}- Since
Resultissealed, we can pattern-match using an exhaustiveswitch-casestatement. This ensures that all possible cases are handled.
-
Explicit Handling: Forces developers to explicitly handle success and failure cases, reducing the likelihood of ignoring error conditions.
-
No Unchecked Exceptions: Unlike traditional exception handling, the
Resultpattern doesn't rely on unchecked exceptions, which can lead to runtime errors. Instead, it encapsulates the success or failure state in a well-defined structure. -
Immutable: The
Resultinstances are immutable, meaning their state cannot be changed once they are created. This helps in ensuring consistency and avoiding unexpected modifications. -
Type-Safe: The type parameter
Tallows theResultinstances to carry a specific type of value, ensuring type safety throughout the code.