Update Result and Failure authored by Andrew Januszko's avatar Andrew Januszko
# What is `Failure`? # What is `Result`?
`Failure` is a wrapper object for `Exception`, allowing you to `catch` exceptions and turn them into and object that is easier to handle. `Failure` works by holding a list of objects called `properties`. This list can contain anything from the exception itself to stack traces or custom error messages. This list of properties allows `Failure` objects to be compared using `Equatable` so we can adapt our behavior on a runtime basis. `Result` is a wrapper for `Future` responses. Used to prevent crashes caused by runtime exceptions. Result can either be an instance of `ResultData` or `ResultFailure`. If an exception is thrown, then `Result` is an instance of `ResultFailure`. If data is received, then `Result` is an instance of `ResultData`.
# How can I use `Failure` in my code? ```dart
@freezed
Right now, we only have 3 types of failures: class Result<T> with _$Result<T> {
1. `GeneralFailure(Object? exception, StackTrace? stackTrace)` ///
2. `HTTPFailure(String message)` /// Creates an instance of `ResultData` from data `T`.
3. `LocationAccessFailure(String message)` ///
factory Result.data({
Each type of `Failure` has a specific use case, and should be used with `Result<T>`, so an example will be provided at the bottom of the page. required T data,
}) = ResultData;
# What is `Result<T>`?
///
`Result<T>` is a wrapper object for any type of response we expect from an asynchronous data source. Since the data source is asynchronous, there is a possibility we could time out or throw an exception, and since we don't want the app to crash we need to catch that somehow. `Result<T>` solves this by wrapping responses and identifying itself as `T data` or `Failure failure `. This prevents runtime exceptions when running asynchronous queries and guarantees that we always get a response back no matter what happens. /// Creates and instance of `ResultFailure` from failure `Failure`.
///
# How does `Result<T>` work? factory Result.failure({
required Failure failure,
`Result<T>` works by having two constructors: `Result.data({required T data})` and `Result.failure({required Failure failure})`. }) = ResultFailure;
- `Result.data({required T data})` tells `Result<T>` that you are an instance of `T data` and that whatever you contain is a valid response.
- `Result.failure({required Failure failure})` tells `Result<T>` that you are an instance of `Failure` and that whatever you contain is an invalid response and should be handled.
These checks allow us to create dud data on the fly when a query fails.
# How does this all come together?
Using `Result<T>` and `Failure` together can best be seen in the `LoginRepositoryHTTP` when trying to log a player in.
**LoginRepositoryHTTP**
```
/// Query the login server.
Future<Result<LoginWithCredentialsResponse>> loginWithCredentials(
{required LoginWithCredentialsRequest request,
}) async {
try {
final response = await loginWithCredentialsDatasource.loginWithCredentials(request: request);
return Result.data(data: response);
} catch (exception, stackTrace) {
return Result.failure(
failure: HTTPFailure(
message: '$exception : $stackTrace',
),
);
}
} }
``` ```
First, we query the login server, but since we don't know if the server is actually up, we could throw an exception. So by wrapping the `await loginWithCredentialsDatasource.loginWithCredentials` function in a try catch we can break the logic into two parts: we succeed or we fail. We can then create a `Result<T>` from this logic stating that if we get a response, we can make a `Result.data`, and if we fail, we can make a `Result.error`. The underlying boilerplate code for `Result` is autogenerated by a package called `Freezed`, so the actual `ResultData` and `ResultFailure` types are not exposed to the user. However, both types will present as the generic type `Result` in code and can be checked using a `.when` statement as seen in the example below.
This information can then be unwrapped using a `when` check, as shown in the following example: ```dart
///
**LoginController** /// This portion of code is snagged from the `LoginController` and best
``` /// represents how to use `Result`.
/// Query the login repository ///
final response = await _repository.loginWithCredentials( /// Assume `LoginWithCredentialsResponse` is an object that holds an
request: LoginWithCredentialsRequest( /// int named `playerID`. Assume `response` is an instance of
username: username, /// `Result<LoginWithCredentialsResponse>`
password: password, ///
),
);
/// When we get a response, use it to set the local response.
LoginWithCredentialsResponse loginResponse = await response.when( LoginWithCredentialsResponse loginResponse = await response.when(
// Here, the `.when` statement allows us to unwrap the response and handle
// the cases when it is an instance of `ResultData` or `ResultFailure`.
data: (data) => data, data: (data) => data,
failure: (failure) => const LoginWithCredentialsResponse( failure: (failure) => const LoginWithCredentialsResponse(
playerID: -1, playerID: -1,
...@@ -69,10 +42,24 @@ LoginWithCredentialsResponse loginResponse = await response.when( ...@@ -69,10 +42,24 @@ LoginWithCredentialsResponse loginResponse = await response.when(
); );
``` ```
First, we query the login repository, which was shown above. This provides us with a response, however, we don't know if this response is valid or not, so we need to check. # What is `Failure`?
`Failure` is a wrapper for `Exception` objects, allowing them to be translated to useable objects in code. Our template for `Failure` has a list of objects known as properties, which can hold anything from the exception itself to a string message or a stack trace.
This is where we use the `when` check, which breaks our response into two parts: what do we do when we get data vs when we get a failure? When we get a failure we can create a dud response to make it seem like we actually got one.
```dart
# Why do we use `Result<T>` and `Failure`? abstract class Failure extends Equatable {
final List<Object?> properties;
These two components used together allow us to handle runtime exceptions and prevent the UI from crashing when something goes wrong. It also allows us to account for circumstances where the servers are down or something behaves incorrectly.
///
/// Constructor.
///
const Failure({
required this.properties,
});
///
/// Get the properties.
///
@override
List<Object?> get props => properties;
}
```
\ No newline at end of file