freezed_result 1.0.3 copy "freezed_result: ^1.0.3" to clipboard
freezed_result: ^1.0.3 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.
14
likes
120
pub points
82%
popularity

Publisher

verified publisherdaylogger.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

MIT (LICENSE)

Dependencies

collection, meta

More

Packages that depend on freezed_result