Last updated:
0 purchases
freezed result
Freezed Result #
A Result<Success, Failure> that feels like a Freezed union. It represents the output of an action that can succeed or fail. It holds either a value of type Success or an error of type Failure.
Failure can be any type, and it usually represents a higher abstraction than just Error or Exception. It's very common to use a Freezed Union for Failure (e.g. AuthFailure) with cases for the different kinds of errors that can occur (e.g. AuthFailure.network, AuthFailure.storage, AuthFailure.validation).
Because of this, we've made Result act a bit like a Freezed union (it has when(success:, failure:)). The base class was generated from Freezed, then we removed the parts that don't apply (maybe*) and adapted the others (map*) to feel more like a Result. We'll get into the details down below.
Usage #
There are 3 main ways to interact with a Result: process it, create it, and transform it.
Processing Values and Errors #
Process the values by handling both success and failure cases using when. This is preferred since you explicitly handle both cases.
final result = fetchPerson(12);
result.when(
success: (person) => state = MyState.personFound(person);
failure: (error) => state = MyState.error(error);
);
copied to clipboard
Or create a common type from both cases, also using when.
final result = fetchPerson(12);
final description = result.when(
success: (person) => 'Found Person ${person.id}';
failure: (error) => 'Problem finding a person.';
);
copied to clipboard
Or ignore the error and do something with maybeValue, which returns null on failures.
final person = result.maybeValue;
if (person != null) {}
copied to clipboard
Or ignore both the value and the error by simply using the outcome.
if (result.isSuccess) {}
// elsewhere
if (result.isFailure) {}
copied to clipboard
Or throw failure cases and return success cases using valueOrThrow.
try {
final person = result.valueOrThrow();
} on ApiFailure catch(e) {
// handle ApiFailure
}
copied to clipboard
Creating Results #
Create the result with named constructors Result.success and Result.failure.
Result.success(person)
copied to clipboard
Result.failure(AuthFailure.network())
copied to clipboard
Declare both the Success and Failure types with typed variables or function return types.
Result<Person, AuthFailure> result = Result.success(person);
copied to clipboard
Result<Person, AuthFailure> result = Result.failure(AuthFailure.network());
copied to clipboard
Result<Person, FormatException> parsePerson(String json) {
return Result.failure(FormatException());
}
copied to clipboard
Results are really useful as return values for async operations.
Future<Result<Person, ApiFailure>> fetchPerson(int id) async {
try {
final person = await api.getPerson(12);
return Result.success(person);
} on TimeoutException {
return Result.failure(ApiFailure.timeout());
} on FormatException {
return Result.failure(ApiFailure.invalidData());
}
}
copied to clipboard
Sometimes you have a function which may have errors, but returns void when successful. Variables can't be void, so use Nothing instead. The singleton instance is nothing.
Result<Nothing, DatabaseError> vacuumDatabase() {
try {
db.vacuum();
return Result.success(nothing);
} on DatabaseError catch(e) {
return Result.failure(e);
}
}
copied to clipboard
You can use catching to create a success result from the return value of a closure. Unlike the constructors, you'll need to await the return value of this call.
Without an explicit type parameters, any Object thrown by the closure is caught and returned in a failure result.
final Result<String, Object> apiResult = await Result.catching(() => getSomeString());
copied to clipboard
With type parameters, only that specific type will be caught. The rest will pass through uncaught.
final result = await Result.catching<String, FormatException>(
() => formatTheThing(),
);
copied to clipboard
Transforming Results #
Process and transform this Result into another Result as needed.
map #
Change the type and value when the Result is a success. Leave the error untouched when it's a failure. Most useful for transformations of success data in a pipeline with steps that will never fail.
Result<DateTime, ApiFailure> bigDay = fetchPerson(12).map((person) => person.birthday);
copied to clipboard
mapError #
Change the error when the Result is a failure. Leave the value untouched when it's a success. Most useful for transforming low-level exceptions into more abstact failure classes which classify the exceptions.
Result<Person, ApiError> apiPerson(int id) {
final Result<Person, DioError> raw = await dioGetApiPerson(12);
return raw.mapError((error) => _interpretDioError(error));
}
copied to clipboard
mapWhen #
Change both the error and the value in one step. Rarely used.
Result<Person, DioError> fetchPerson(int id) {
// ...
}
Result<String, ApiFailure> fullName = fetchPerson(12).mapWhen(
success: (person) => _sanitize(person.firstName, person,lastName),
failure: (error) => _interpretDioError(error),
);
copied to clipboard
mapToResult #
Use this to turn a success into either another success or to a compatible failure. Most useful when processing the success value with another operation which may itself fail.
final Result<Person, FormatError> personResult = parsePerson(jsonString);
final Result<DateTime, FormatError> bigDay = personResult.mapToResult(
(person) => parse(person.birthDateString),
);
copied to clipboard
Parsing the Person may succeed, but parsing the DateTime may fail. In that case, an initial success is transformed into a failure. Aliased to flatMap as well for newcomers from Swift.
mapErrorToResult #
Use this to turn an error into either a success or another error. Most useful for recovering from errors which have a workaround.
Here, mapErrorToResult is used to ignore errors which can be resolved by a cache lookup. An initial failure is transformed into a success whenever the required value is available in the local cache. The _getPersonCache function also translates both unrecoverable original DioErrors, and any internal errors accessing the cache, into the more generic FetchError.
final Result<Person, DioError> raw = await dioGetApiPerson(id);
final Result<Person, FetchError> output = raw.mapErrorToResult((error) => _getPersonCache(id, error));
Result<Person, FetchError> _getPersonCache(int id, DioError error) {
// ...
}
copied to clipboard
Aliased to flatMapError for Swift newcomers.
mapToResultWhen #
Rarely used. This allows a single action to both try another operation on a success value which may fail in a new way with a new error type, and to recover from any original error with a success or translate the error into the new type of Failure.
Result<Person, DioError> fetchPerson(int id) {
// ...
}
Result<String, ProcessingError> fullName = fetchPerson(12).mapToResultWhen(
success: (person) => _fullName(person.firstName, person,lastName),
failure: (dioError) => _asProcessingError(dioError),
);
copied to clipboard
Aliased to flatMapWhen, though Swift doesn't have this equivalent.
Alternatives #
Result matches most of Swift's Result type.
result_type which fully matches Swift, and some Rust.
fluent_result allows multiple errors in a failure, and allows custom errors by extending a ResultError class.
Dartz is a functional programming package whose Either type can be used as a substitute for Result. It has no concept of success and failure. Instead it uses left and right. It uses the functional name fold to accomplish what we do with when.
error_or is focused more on error handling, and defines only the success type; failure is always Object.
result_class similar to Rust result.
result_monad also modeled on Rust, but with a strong focus on mapping.
rust_like_result also inspired by Rust.
simple_result inspired by Swift and Freezed. Also uses when like freezed_result.
Super Enum is a library with a larger goal, but it shows how to roll your own Result with the library.
For personal and professional use. You cannot resell or redistribute these repositories in their original state.
There are no reviews.