freezed_result 1.0.3+1 icon indicating copy to clipboard operation
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 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.
4
likes
130
pub points
55%
popularity

Publisher

verified publisher icondaylogger.dev

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.

Repository (GitHub)
View/report issues

Documentation

API reference

License

Icon for licenses.MIT (LICENSE)

Dependencies

collection, meta

More

Packages that depend on freezed_result