freezed_result 1.0.3+1 copy "freezed_result: ^1.0.3+1" to clipboard
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);
);
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.
16
likes
140
points
16
downloads

Publisher

verified publisherdaylogger.dev

Weekly Downloads

2024.09.22 - 2025.04.06

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)

Documentation

API reference

License

MIT (license)

Dependencies

collection, meta

More

Packages that depend on freezed_result