Update Result and Failure authored by Andrew Januszko's avatar Andrew Januszko
# What is `Failure`?
`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.
# How can I use `Failure` in my code?
Right now, we only have 3 types of failures:
1. `GeneralFailure(Object? exception, StackTrace? stackTrace)`
2. `HTTPFailure(String message)`
3. `LocationAccessFailure(String message)`
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.
# 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.
# How does `Result<T>` work?
`Result<T>` works by having two constructors: `Result.data({required T data})` and `Result.failure({required Failure failure})`.
- `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.
# What is `Failure`?
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`.
This information can then be unwrapped using a `when` check, as shown in the following example:
**LoginController**
```
/// Query the login repository
final response = await _repository.loginWithCredentials(
request: LoginWithCredentialsRequest(
username: username,
password: password,
),
);
/// When we get a response, use it to set the local response.
LoginWithCredentialsResponse loginResponse = await response.when(
data: (data) => data,
failure: (failure) => const LoginWithCredentialsResponse(
playerID: -1,
),
);
```
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.
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.
# Why do we use `Result<T>` and `Failure`?
# How can I use `Result<T>` in my code?
\ No newline at end of file
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.