fluent_result 8.5.0
fluent_result: ^8.5.0 copied to clipboard
A fluent Result type for Dart that returns success/failure instead of throwing, with a global observability seam for crash reporting.
fluent_result #
fluent_result is a lightweight Dart library developed to solve a common problem. It returns an object indicating success or failure of an operation instead of throwing/using exceptions.
- Store multiple errors in one Result object
- Store powerful and elaborative Error object instead of only error messages in string format
- Designing Errors in an object-oriented way
Usage #
Creating a Result #
Create a result which indicates success
Result result = Result.success();
Result sameResult = Result.ok;
Result sameResult2 = success();
Create a result which indicates failure
Result errorResult1 = Result.failWith('a fail reason');
Result errorResult2 = Result.failWith(ResultError('my error message'));
Result errorResult3 = Result.failWith(MyException('exception description'));
Result errorResult4 = Result.failWith(['a fail reason', ResultError('my error message')]);
Result same = fail(MyException('exception description'));
Generic ResultOf<T> #
Success result with value:
ResultOf<MyObject> result = ResultOf.success(MyObject());
ResultOf<MyObject> sameResult = successWith(MyObject());
MyObject? value = result.value;
Fail result with error and without value:
ResultOf<MyObject> result = ResultOf.failWith<MyObject>(ResultError('a fail reason'));
MyObject? value = result.value; // is null because of the fail result
failIf() and okIf() #
With the methods failIf() and okIf() you can also write in a more readable way:
final result1 = Result.failIf(() => firstName.isEmpty, "First Name is empty");
final result2 = Result.okIf(() => firstName.isNotEmpty, 'First name should not be empty');
Try #
Sync
final res = Result.trySync(() {
throw 'Some exception';
});
res.isFail.should.beTrue();
res.errorMessage.should.be('Some exception');
Async
final res = await Result.tryAsync(() async {
await Future.delayed(const Duration(seconds: 2));
print('Done');
});
res.isSuccess.should.beTrue();
Fold #
Result res = fail('error reason');
res.fold(
onFail: (errors) {
// process errors
},
onSuccess: () {
// process success path
},
);
ResultOf<String> resultWithData = successWith('someData');
resultWithData.foldWithValue(
onFail: (errors) {
// process errors
},
onSuccess: (data) {
// process success path with data
},
);
Converting Result to another #
To convert one success result to another success result has to be provided a valueConverter function.
final anotherResult =
result.map((customer) => User(customer.id));
To convert one fail result to another fail result
final anotherResult = failResult.map<Customer>();
Combinators #
ResultOf<T> provides combinators for composing results without unwrapping:
// chain a success into another Result (errors pass through on a fail)
final r = successWith(2).flatMap((v) => successWith(v * 10)); // ResultOf(20)
// collapse into a value (the value-returning counterpart to foldWithValue)
final label = r.match(
onFail: (errors) => 'failed: ${errors.first.message}',
onSuccess: (value) => 'ok: $value',
);
// extract with a fallback (eager or lazy)
final value = r.valueOr(0);
final lazy = r.getOrElse(() => compute());
// recover a fail into a success, or transform every error
final recovered = failResult.recover((errors) => 0);
final remapped = failResult.mapError((e) => ResultError('[${e.message}]'));
Wrap a plain, possibly-throwing computation with guard / guardAsync — the body returns a
bare value, not a pre-lifted Result:
final parsed = ResultOf.guard(() => int.parse(input));
final fetched = await ResultOf.guardAsync(() => api.fetch(id));
Custom errors #
To make your codebase more robust. Create your own error collection of the App by extending ResultError.
class InvalidPasswordError extends ResultError {
const InvalidPasswordError(String message)
: super(message);
}
class CustomerNotFound extends ResultError {
const CustomerNotFound({
required this.customerId,
}) : super('Customer not found with ID $customerId');
final int customerId;
@override
String toString() => message;
}
Collect errors #
For example, easy to work with errors which comes from HTTP API.
final err1 = CustomerNotFound(customerId: 1);
final res = Result.failWith(err1);
final err2 = InvalidPasswordError('The password 123456 is invalid');
res.add(err2);
res.contains<InvalidPasswordError>(); // true
res.get<InvalidPasswordError>().should.not.beNull();
Observability (crash reporting) #
fluent_result keeps two error paths separate:
- Validation (
failIf/okIf) — deliberate, expected failures. These are never reported. - Caught exceptions (
trySync/tryAsync/guard/guardAsync) — unexpected throws. These are reported once toResultConfig.onException.
Wire your crash reporter once at startup (no-op by default, so nothing is reported until you do):
ResultConfig.onException = (error, stack) {
Sentry.captureException(error, stackTrace: stack);
};
Classify errors with ResultMatcher. Matchers are subtype-aware (e is T) and the first
match wins. Flag expected control flow (offline, cancellation, 404, …) as expected: true so it
fails quietly without reaching onException:
ResultConfig.matchers = [
// expected control flow — built into a fail Result, NOT reported
ResultMatcher((e) => e is SocketException, (e, st) => fail(e), expected: true),
// map a third-party error to a typed ResultError (reported, since not expected)
ResultMatcher((e) => e is DioException, (e, st) => fail(DioErrorResult(e))),
];
Some third-party errors extend
Errorrather thanException(e.g.DioError extends Error), so match them with(e) => e is DioError(ore is Error) — ane is Exceptioncatch-all will not match them.
Call ResultConfig.reset() in your test tearDown to keep this global config from leaking
between tests.
Migrating from exceptionHandler / exceptionHandlerMatchers
Both are deprecated but still work — prefer onException + matchers. If your old
exceptionHandler reported errors itself, move that reporting into onException; otherwise an
unexpected error is reported twice (once by onException, once by your handler).
Contributing #
We accept the following contributions:
- Improving documentation
- Reporting issues
- Fixing bugs