Skip to content

lucafabbri/flutter-error-or

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

36 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

ErrorOr Plus

This library is a porting of ErrorOr for C# made by Amichai Mantinband

A simple, fluent discriminated union of an error or a result.

dart pub add error_or_plus

Give it a star ⭐!

Loving it? Show your support by giving this project a star!

Getting Started πŸƒ

Replace throwing exceptions with ErrorOr<T>

This πŸ‘‡

double divide(int a, int b)
{
    if (b == 0)
    {
        throw Exception("Cannot divide by zero");
    }

    return a / b;
}

try
{
    var result = divide(4, 2);
    print(result * 2); // 4
}
on Exception catch (e)
{
    print(e);
    return;
}

Turns into this πŸ‘‡

ErrorOr<double> divide(int a, int b)
{
    if (b == 0)
    {
        return Errors.unexpected(description: "Cannot divide by zero");
    }

    return a / b;
}

var result = divide(4, 2);

if (result.isError)
{
    print(result.firstError.description);
    return;
}

print(result.value * 2); // 4

Or, using also/orElse and doSwitch/match, you can do this πŸ‘‡

divide(4, 2)
    .also((val) => val * 2)
    .doSwitchFirst(
        onValue: print, // 4
        onFirstError: (error) => print(error.description));

Support For Multiple errors

Internally, the ErrorOr object has a list of Errorss, so if you have multiple errors, you don't need to compromise and have only the first one.

class User
{
    final String _name;

    User._internal(this._name);

    static ErrorOr<User> create(String name)
    {
        List<Errors> errors = [];

        if (name.length < 2)
        {
            errors.add(Errors.validation(description: "Name is too short"));
        }

        if (name.length > 100)
        {
            errors.add(Errors.validation(description: "Name is too long"));
        }

        if (name.isEmpty)
        {
            errors.add(Errors.validation(description: "Name cannot be empty or whitespace only"));
        }

        if (errors.isNotEmpty)
        {
            return errors.toErrorOr<User>();
        }

        return User._internal(name).toErrorOr();
    }
}

Various Functional Methods and Extension Methods

The ErrorOr object has a variety of methods that allow you to work with it in a functional way.

This allows you to chain methods together, and handle the result in a clean and concise way.

Real world example

return await _userRepository.getByIdAsync(id)
    .also((user) => user.incrementAge()
        .also((success) => user)
        .orElse(errorOnErrorHandler: (errors) => Errors.unexpected("Not expected to fail")))
    .failIf((user) => !user.isOverAge(18), UserErrors.underAge)
    .alsoDo((user) => _logger.logInformation("User ${user.Id} incremented age to ${user.Age}"))
    .alsoAsync((user) => _userRepository.updateAsync(user))
    .match(
        (_) => noContent(),
        (errors) => errors.toActionResult());

Simple Example with intermediate steps

No Failure

ErrorOr<String> foo = await "2".toErrorOr()
    .also(int.parse) // 2
    .failIf((val) => val > 2, Errors.validation(description: "$${val} is too big") // 2
    .alsoDoAsync((val) => Future.delayed(Duration(milliseconds: val))) // Sleep for 2 milliseconds
    .alsoDo((val) => print("Finished waiting $${val} milliseconds.")) // Finished waiting 2 milliseconds.
    .alsoAsync((val) => Future.value(val * 2)) // 4
    .also((val) => "The result is $${val}") // "The result is 4"
    .orElse(errorOnErrorHandler: (errors) => Errors.unexpected(description: "Yikes")) // "The result is 4"
    .matchFirst(
        (value) => value, // "The result is 4"
        (firstError) => "An error occurred: ${firstError.description}");

Failure

ErrorOr<String> foo = await "5".ToErrorOr()
    .also(int.Parse) // 5
    .failIf((val) => val > 2, Errors.validation(description: "${val} is too big")) // Errors.validation()
    .alsoDoAsync((val) => Future.delayed(Duration(milliseconds: val))) // Errors.validation()
    .alsoDo((val) => print("Finished waiting ${val} milliseconds.")) // Errors.validation()
    .alsoAsync((val) => Future.value(val * 2)) // Errors.validation()
    .also((val) => "The result is ${val}") // Errors.validation()
    .orElse(errorOnErrorHandler: (errors) => Errors.unexpected(description: "Yikes")) // Errors.unexpected()
    .matchFirst(
        (value) => value,
        (firstError) => "An error occurred: {firstError.description}"); // An error occurred: Yikes

Creating an ErrorOr instance

Using The ToErrorOr Extension Method

ErrorOr<int> result = 5.ToErrorOr();
ErrorOr<int> result = Errors.unexpected().ToErrorOr<int>();
ErrorOr<int> result = [Errors.validation(), Errors.validation()].ToErrorOr<int>();

Properties

isError

ErrorOr<int> result = User.create();

if (result.isError)
{
    // the result contains one or more errors
}

value

ErrorOr<int> result = User.create();

if (!result.isError) // the result contains a value
{
    print(result.value);
}

errors

ErrorOr<int> result = User.create();

if (result.isError)
{
    result.errors // contains the list of errors that occurred
        .forEach((error) => print(error.description));
}

firstError

ErrorOr<int> result = User.create();

if (result.isError)
{
    var firstError = result.firstError; // only the first error that occurred
    print(firstError == result.errors[0]); // true
}

errorsOrEmptyList

ErrorOr<int> result = User.create();

if (result.isError)
{
    result.errorsOrEmptyList // List<Errors> { /* one or more errors */  }
    return;
}

result.errorsOrEmptyList // List<Errors> { }

Methods

match

The match method receives two functions, onValue and onError, onValue will be invoked if the result is success, and onError is invoked if the result is an error.

match

String foo = result.match(
    (value) => value,
    (errors) => "${errors.Count} errors occurred.");

matchAsync

String foo = await result.matchAsync(
    (value) => Future.value(value),
    (errors) => Future.value("${errors.Count} errors occurred."));

matchFirst

The matchFirst method receives two functions, onValue and onError, onValue will be invoked if the result is success, and onError is invoked if the result is an error.

Unlike match, if the state is error, matchFirst's onError function receives only the first error that occurred, not the entire list of errors.

String foo = result.matchFirst(
    (value) => value,
    (firstError) => firstError.description);

matchFirstAsync

String foo = await result.matchFirstAsync(
    (value) => Future.value(value),
    (firstError) => Future.value(firstError.description));

doSwitch

The doSwitch method receives two actions, onValue and onError, onValue will be invoked if the result is success, and onError is invoked if the result is an error.

doSwitch

result.doSwitch(
    (value) => print(value),
    (errors) => print("${errors.Count} errors occurred."));

doSwitchAsync

await result.doSwitchAsync(
    (value) { print(value); return Future.value(true); },
    (errors) { print("${errors.Count} errors occurred."); return Future.value(true); });

doSwitchFirst

The doSwitchFirst method receives two actions, onValue and onError, onValue will be invoked if the result is success, and onError is invoked if the result is an error.

Unlike doSwitch, if the state is error, doSwitchFirst's onError function receives only the first error that occurred, not the entire list of errors.

result.doSwitchFirst(
    (value) => print(value),
    (firstError) => print(firstError.description));

doSwitchFirstAsync

await result.doSwitchFirstAsync(
    (value) { print(value); return Future.value(true); },
    (firstError) { print(firstError.description); return Future.value(true); });

also

also

also receives a function, and invokes it only if the result is not an error.

ErrorOr<int> foo = result
    .also((val) => val * 2);

Multiple also methods can be chained together.

ErrorOr<String> foo = result
    .also((val) => val * 2)
    .also((val) => "The result is ${val}");

If any of the methods return an error, the chain will break and the errors will be returned.

ErrorOr<int> Foo() => Errors.unexpected();

ErrorOr<String> foo = result
    .also((val) => val * 2)
    .also((_) => getAnError())
    .also((val) => "The result is ${val}") // this function will not be invoked
    .also((val) => "The result is ${val}"); // this function will not be invoked

alsoAsync

alsoAsync receives an asynchronous function, and invokes it only if the result is not an error.

ErrorOr<String> foo = await result
    .alsoAsync((val) => doSomethingAsync(val))
    .alsoAsync((val) => doSomethingElseAsync("The result is ${val}"));

alsoDo and alsoDoAsync

alsoDo and alsoDoAsync are similar to also and alsoAsync, but instead of invoking a function that returns a value, they invoke an action.

ErrorOr<String> foo = result
    .alsoDo((val) => print(val))
    .alsoDo((val) => print("The result is ${val}"));
ErrorOr<String> foo = await result
    .alsoDoAsync((val) => Future.delayed(Duration(milliseconds: val)))
    .alsoDo((val) => print("Finsihed waiting ${val} seconds."))
    .alsoDoAsync((val) => Future.value(val * 2))
    .alsoDo((val) => "The result is ${val}");

Mixing also, alsoDo, alsoAsync, alsoDoAsync

You can mix and match also, alsoDo, alsoAsync, alsoDoAsync methods.

ErrorOr<String> foo = await result
    .alsoDoAsync((val) => Future.delayed(Duration(milliseconds: val)))
    .also((val) => val * 2)
    .alsoAsync((val) => doSomethingAsync(val))
    .alsoDo((val) => print("Finsihed waiting ${val} seconds."))
    .alsoAsync((val) => Future.value(val * 2))
    .also((val) => "The result is ${val}");

failIf

failIf receives a predicate and an error. If the predicate is true, failIf will return the error. Otherwise, it will return the value of the result.

ErrorOr<int> foo = result
    .failIf((val) => val > 2, Errors.validation(description: "${val} is too big"));

Once an error is returned, the chain will break and the error will be returned.

var result = "2".ToErrorOr()
    .also(int.Parse) // 2
    .failIf((val) => val > 1, Errors.validation(description: "${val} is too big") // validation error
    .also(num => num * 2) // this function will not be invoked
    .also(num => num * 2) // this function will not be invoked

orElse

orElse receives a value or a function. If the result is an error, orElse will return the value or invoke the function. Otherwise, it will return the value of the result.

orElse

ErrorOr<String> foo = result
    .orElse(valueOnError: "fallback value");
ErrorOr<String> foo = result
    .orElse(valueOnErrorHandler: (errors) => "${errors.Count} errors occurred.");

orElseAsync

ErrorOr<String> foo = await result
    .orElseAsync(valueOnError: Future.value("fallback value"));
ErrorOr<String> foo = await result
    .orElseAsync(valueOnErrorHandler: (errors) => Future.value("${errors.Count} errors occurred."));

Mixing Features (also, failIf, orElse, doSwitch, match)

You can mix also, failIf, orElse, doSwitch and match methods together.

ErrorOr<String> foo = await result
    .alsoDoAsync((val) => Future.delayed(Duration(milliseconds: val)))
    .failIf((val) => val > 2, Errors.validation(description: "${val} is too big"))
    .alsoDo((val) => print("Finished waiting ${val} seconds."))
    .alsoAsync((val) => Future.value(val * 2))
    .also((val) => "The result is ${val}")
    .orElse(errorOnErrorHandler: (errors) => Errors.unexpected())
    .matchFirst(
        (value) => value,
        (firstError) => "An error occurred: {firstError.description}");

Errors Types

Each Errors instance has a Type property, which is an enum value that represents the type of the error.

Built in error types

The following error types are built in:

enum ErrorType {
  failure,
  unexpected,
  validation,
  conflict,
  notFound,
  unauthorized,
  forbidden,
}

Each error type has a static method that creates an error of that type. For example:

var error = Errors.notFound();

optionally, you can pass a code, description and metadata to the error:

  var user = Object();
  var error = Errors.unexpected(
      code: "User.ShouldNeverHappen",
      description: "A user error that should never happen",
      metadata: Map.fromEntries([
        MapEntry("user", user),
      ]));

The ErrorType enum is a good way to categorize errors.

Organizing errors

A nice approach, is creating a static class with the expected errors. For example:

class DivisionErrors
{
    static Errors cannotdivideByZero = Errors.unexpected(
        code: "Division.CannotdivideByZero",
        description: "Cannot divide by zero.");
}

Which can later be used as following πŸ‘‡

public ErrorOr<double> divide(int a, int b)
{
    if (b == 0)
    {
        return DivisionErrors.cannotdivideByZero;
    }

    return a / b;
}

Contribution 🀲

If you have any questions, comments, or suggestions, please open an issue or create a pull request πŸ™‚

Credits πŸ™

  • ErrorOr - An awesome library which provides C# style discriminated unions behavior for C#

License

This project is licensed under the terms of the MIT license.