freezed_result 1.0.3+1
freezed_result: ^1.0.3+1 copied to clipboard
A Result<Success, Failure> that feels like a Freezed union. It holds the outcome of an operation—a value of type Success, or an error of type Failure—and methods to work with it.
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);
);
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.';
);
Or ignore the error and do something with maybeValue, which returns null on failures.
final person = result.maybeValue;
if (person != null) {}
Or ignore both the value and the error by simply using the outcome.
if (result.isSuccess) {}
// elsewhere
if (result.isFailure) {}
Or throw failure cases and return success cases using valueOrThrow.
try {
final person = result.valueOrThrow();
} on ApiFailure catch(e) {
// handle ApiFailure
}
Creating Results #
Create the result with named constructors Result.success and Result.failure.
Result.success(person)
Result.failure(AuthFailure.network())
Declare both the Success and Failure types with typed variables or function return types.
Result<Person, AuthFailure> result = Result.success(person);
Result<Person, AuthFailure> result = Result.failure(AuthFailure.network());
Result<Person, FormatException> parsePerson(String json) {
return Result.failure(FormatException());
}
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());
}
}
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);
}
}
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());
With type parameters, only that specific type will be caught. The rest will pass through uncaught.
final result = await Result.catching<String, FormatException>(
() => formatTheThing(),
);
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);
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));
}
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),
);
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),
);
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) {
// ...
}
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),
);
Aliased to flatMapWhen, though Swift doesn't have this equivalent.
Alternatives #
- Result matches most of Swift's
Resulttype. - result_type which fully matches Swift, and some Rust.
- fluent_result allows multiple errors in a failure, and allows custom errors by extending a
ResultErrorclass. - Dartz is a functional programming package whose
Eithertype can be used as a substitute forResult. It has no concept of success and failure. Instead it usesleftandright. It uses the functional namefoldto accomplish what we do withwhen. - 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
whenlikefreezed_result. - Super Enum is a library with a larger goal, but it shows how to roll your own Result with the library.