-
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;
}
-
Result
is an abstract class with a single boolean propertyisSuccess
. This property indicates whether the operation represented by the result was successful (true
) or not (false
). -
Result
is marked assealed
, meaning it cannot be extended outside of its library. This allows us to use exhaustive pattern-matching usingswitch-case
statements 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;
}
-
Success
represents a successful outcome and contains a value of typeT
. It extendsResult
and setsisSuccess
totrue
.
final class Failure<T> extends Result<T> {
const Failure(this.exception, [this.stackTrace]) : super(isSuccess: false);
final Exception exception;
final StackTrace? stackTrace;
}
-
Failure
represents a failure and contains an exception (and an optional stack trace) indicating why the operation failed. It extendsResult
and setsisSuccess
tofalse
.
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
Success
instance containing the post. - If the operation fails, the function returns a
Failure
instance 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
Result
issealed
, we can pattern-match using an exhaustiveswitch-case
statement. 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
Result
pattern 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
Result
instances 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
T
allows theResult
instances to carry a specific type of value, ensuring type safety throughout the code.