Skip to content

Commit af6925e

Browse files
higorlapaclaude
andcommitted
feat: add v5.3.0 improvements with AsyncResult and new functional methods
- Fix Error.getOrThrow() to pass <S, E> to SuccessResultNotFoundException - Add Result.tryCatch static factory for wrapping throwing functions - Add getOrElse, onSuccess, onError, flatMapError, recover, swap methods - Add AsyncResult<S,E> zero-cost extension type for async chaining - Bump version to 5.3.0, tighten SDK constraint to >=3.3.0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent b8dad16 commit af6925e

File tree

8 files changed

+784
-3
lines changed

8 files changed

+784
-3
lines changed

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1+
## 5.3.0 03/14/2026
2+
3+
### Bug Fixes
4+
* `Error.getOrThrow()` now correctly passes `<S, E>` to `SuccessResultNotFoundException`, so `toString()` shows the actual types instead of `dynamic`.
5+
6+
### New Features
7+
* `Result.tryCatch(action, onError)` — static factory that wraps a potentially-throwing synchronous computation.
8+
* `getOrElse(orElse)` — returns the success value or a fallback computed from the error.
9+
* `onSuccess(action)` / `onError(action)` — side-effect tap methods that execute an action without transforming the result, returning `this` for chaining.
10+
* `flatMapError(mapper)` — symmetric counterpart to `flatMap` for the error channel.
11+
* `recover(mapper)` — alias for `flatMapError`.
12+
* `swap()` — converts `Success` to `Error` and vice versa.
13+
* `AsyncResult<S, E>` — zero-cost `Future<Result<S, E>>` wrapper via Dart extension types, with a full mirrored API (`mapSuccess`, `mapError`, `map`, `flatMap`, `flatMapAsync`, `flatMapError`, `swap`, `onSuccess`, `onError`, `getOrThrow`, `getOrElse`, `tryGetSuccess`, `tryGetError`, `when`, `isSuccess`, `isError`, and `tryCatch`).
14+
* `AsyncResultOf<S, E>` typedef alias for `AsyncResult<S, E>`.
15+
116
## 5.2.0 04/29/2025
217

318
* Improves README.md

README.md

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,91 @@ The key difference between `mapSuccess` and `flatMap`:
251251
- `mapSuccess` takes a function that returns a value `T` and wraps it in a Result
252252
- `flatMap` takes a function that returns a Result directly, avoiding nested Results
253253

254+
### Using `Result.tryCatch`
255+
256+
Replace try/catch blocks with `Result.tryCatch` to convert a throwing operation directly into a `Result`:
257+
258+
```dart
259+
final result = Result.tryCatch(
260+
() => int.parse(userInput),
261+
(error, stackTrace) => ParseError(error.toString()),
262+
);
263+
// result is Success(42) or Error(ParseError(...))
264+
```
265+
266+
### Using `getOrElse`
267+
268+
Provide a fallback value computed from the error instead of throwing:
269+
270+
```dart
271+
final value = result.getOrElse((error) => defaultValue);
272+
```
273+
274+
### Using `onSuccess` / `onError`
275+
276+
Run side effects (e.g. logging) in the middle of a chain without breaking it:
277+
278+
```dart
279+
result
280+
.onSuccess((data) => logger.info('Got $data'))
281+
.onError((error) => logger.error('Failed: $error'))
282+
.mapSuccess((data) => data.toUpperCase());
283+
```
284+
285+
### Using `flatMapError` / `recover`
286+
287+
Handle errors and recover into a new `Result`, symmetric to `flatMap`:
288+
289+
```dart
290+
final result = fetchUser(id)
291+
.flatMapError((e) => fetchUserFromCache(id));
292+
293+
// or with the alias:
294+
final result = fetchUser(id)
295+
.recover((e) => fetchUserFromCache(id));
296+
```
297+
298+
### Using `swap`
299+
300+
Convert a `Success` into an `Error` and vice versa — useful for inverting validation logic:
301+
302+
```dart
303+
final inverted = result.swap();
304+
// Success('x') becomes Error('x'), Error(e) becomes Success(e)
305+
```
306+
307+
## Async Results with `AsyncResult`
308+
309+
`AsyncResult<S, E>` is a zero-cost wrapper around `Future<Result<S, E>>` using Dart extension types. It lets you chain transformations on async results fluently without awaiting at each step.
310+
311+
```dart
312+
// Wrap a Future<Result> as AsyncResult
313+
AsyncResult<User, ApiError> fetchUser(int id) =>
314+
AsyncResult(api.get('/users/$id').then((r) => r.toResult()));
315+
316+
// Chain operations without intermediate awaits
317+
final greeting = await fetchUser(42)
318+
.mapSuccess((user) => user.name.toUpperCase())
319+
.onError((e) => logger.error('Failed: $e'))
320+
.getOrElse((_) => 'Anonymous');
321+
```
322+
323+
Use `AsyncResult.tryCatch` to wrap a potentially-throwing async function:
324+
325+
```dart
326+
final result = AsyncResult.tryCatch(
327+
() async => await api.fetchUser(id),
328+
(error, stack) => NetworkError(error.toString()),
329+
);
330+
```
331+
332+
Use `flatMapAsync` to chain operations that are themselves async:
333+
334+
```dart
335+
final result = fetchUser(42)
336+
.flatMapAsync((user) => fetchPosts(user.id));
337+
```
338+
254339
## Unit Type
255340

256341
Some results don't need a specific return value. Use the Unit type to signal an empty return:

lib/multiple_result.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ library multiple_result;
22

33
export 'src/result.dart';
44
export 'src/unit.dart';
5+
export 'src/async_result.dart';

lib/src/async_result.dart

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import 'result.dart';
2+
3+
/// A zero-cost wrapper around [Future<Result<S, E>>] using Dart extension types.
4+
///
5+
/// [AsyncResult] lets you chain and transform asynchronous [Result] values
6+
/// using the same API as synchronous [Result], without allocating any
7+
/// intermediate wrapper objects at runtime.
8+
///
9+
/// Example:
10+
/// ```dart
11+
/// AsyncResult<String, Exception> fetchUser(int id) =>
12+
/// AsyncResult(fetchUserJson(id).then((json) => Success(json['name'])));
13+
///
14+
/// final name = await fetchUser(42)
15+
/// .mapSuccess((n) => n.toUpperCase())
16+
/// .getOrElse((_) => 'unknown');
17+
/// ```
18+
extension type AsyncResult<S, E>(Future<Result<S, E>> _future) {
19+
/// Returns the inner [Future<Result<S, E>>].
20+
Future<Result<S, E>> get future => _future;
21+
22+
// ── Transformations ─────────────────────────────────────────────────────────
23+
24+
/// Transforms the success value if this resolves to a [Success].
25+
AsyncResult<U, E> mapSuccess<U>(U Function(S success) mapper) =>
26+
AsyncResult(_future.then((r) => r.mapSuccess(mapper)));
27+
28+
/// Transforms the error value if this resolves to an [Error].
29+
AsyncResult<S, F> mapError<F>(F Function(E error) mapper) =>
30+
AsyncResult(_future.then((r) => r.mapError(mapper)));
31+
32+
/// Transforms both success and error values.
33+
AsyncResult<U, F> map<U, F>({
34+
required U Function(S success) successMapper,
35+
required F Function(E error) errorMapper,
36+
}) =>
37+
AsyncResult(
38+
_future.then(
39+
(r) => r.map(successMapper: successMapper, errorMapper: errorMapper),
40+
),
41+
);
42+
43+
/// Chains an operation that returns a synchronous [Result].
44+
AsyncResult<U, E> flatMap<U>(Result<U, E> Function(S success) mapper) =>
45+
AsyncResult(_future.then((r) => r.flatMap(mapper)));
46+
47+
/// Chains an operation that returns an [AsyncResult].
48+
AsyncResult<U, E> flatMapAsync<U>(
49+
AsyncResult<U, E> Function(S success) mapper,
50+
) =>
51+
AsyncResult(
52+
_future.then(
53+
(r) => switch (r) {
54+
Success(:final success) => mapper(success)._future,
55+
Error(:final error) => Future.value(Result<U, E>.error(error)),
56+
},
57+
),
58+
);
59+
60+
/// Chains error recovery that returns a synchronous [Result].
61+
AsyncResult<S, F> flatMapError<F>(
62+
Result<S, F> Function(E error) mapper,
63+
) =>
64+
AsyncResult(_future.then((r) => r.flatMapError(mapper)));
65+
66+
/// Returns a new [AsyncResult] with [Success] and [Error] swapped.
67+
AsyncResult<E, S> swap() => AsyncResult(_future.then((r) => r.swap()));
68+
69+
// ── Side effects ────────────────────────────────────────────────────────────
70+
71+
/// Executes [action] as a side effect if the result is a [Success],
72+
/// then propagates the result unchanged.
73+
AsyncResult<S, E> onSuccess(void Function(S success) action) =>
74+
AsyncResult(_future.then((r) => r.onSuccess(action)));
75+
76+
/// Executes [action] as a side effect if the result is an [Error],
77+
/// then propagates the result unchanged.
78+
AsyncResult<S, E> onError(void Function(E error) action) =>
79+
AsyncResult(_future.then((r) => r.onError(action)));
80+
81+
// ── Extraction ──────────────────────────────────────────────────────────────
82+
83+
/// Awaits the result and returns the success value, or throws
84+
/// [SuccessResultNotFoundException] if it is an [Error].
85+
Future<S> getOrThrow() => _future.then((r) => r.getOrThrow());
86+
87+
/// Awaits the result and returns the success value, or calls [orElse] with
88+
/// the error and returns that value.
89+
Future<S> getOrElse(S Function(E error) orElse) =>
90+
_future.then((r) => r.getOrElse(orElse));
91+
92+
/// Awaits the result and returns the success value, or null.
93+
Future<S?> tryGetSuccess() => _future.then((r) => r.tryGetSuccess());
94+
95+
/// Awaits the result and returns the error value, or null.
96+
Future<E?> tryGetError() => _future.then((r) => r.tryGetError());
97+
98+
/// Awaits and calls [whenSuccess] or [whenError] based on the result.
99+
Future<W> when<W>(
100+
W Function(S success) whenSuccess,
101+
W Function(E error) whenError,
102+
) =>
103+
_future.then((r) => r.when(whenSuccess, whenError));
104+
105+
/// Awaits and checks whether the result is a [Success].
106+
Future<bool> isSuccess() => _future.then((r) => r.isSuccess());
107+
108+
/// Awaits and checks whether the result is an [Error].
109+
Future<bool> isError() => _future.then((r) => r.isError());
110+
111+
// ── Factory ─────────────────────────────────────────────────────────────────
112+
113+
/// Wraps a potentially-throwing async operation.
114+
///
115+
/// If [action] completes normally, returns an [AsyncResult] wrapping [Success].
116+
/// If [action] throws, calls [onError] and returns an [AsyncResult] wrapping [Error].
117+
///
118+
/// Example:
119+
/// ```dart
120+
/// final result = AsyncResult.tryCatch(
121+
/// () async => await fetchUser(id),
122+
/// (err, stack) => NetworkError(err.toString()),
123+
/// );
124+
/// ```
125+
static AsyncResult<S, E> tryCatch<S, E>(
126+
Future<S> Function() action,
127+
E Function(Object error, StackTrace stackTrace) onError,
128+
) =>
129+
AsyncResult(
130+
Future(() async {
131+
try {
132+
return Success<S, E>(await action());
133+
} catch (e, s) {
134+
return Error<S, E>(onError(e, s));
135+
}
136+
}),
137+
);
138+
}
139+
140+
/// Alias for [AsyncResult].
141+
typedef AsyncResultOf<S, E> = AsyncResult<S, E>;

lib/src/result.dart

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,30 @@ sealed class Result<S, E> {
2020
/// Creates a [Result] that represents a failed operation.
2121
const factory Result.error(E e) = Error;
2222

23+
/// Executes [action] and wraps the result.
24+
///
25+
/// If [action] completes normally, returns [Success] with its return value.
26+
/// If [action] throws, calls [onError] with the caught object and stack trace
27+
/// and returns [Error] with the result.
28+
///
29+
/// Example:
30+
/// ```dart
31+
/// final result = Result.tryCatch(
32+
/// () => int.parse(input),
33+
/// (err, stack) => ParseFailure(err.toString()),
34+
/// );
35+
/// ```
36+
static Result<S, E> tryCatch<S, E>(
37+
S Function() action,
38+
E Function(Object error, StackTrace stackTrace) onError,
39+
) {
40+
try {
41+
return Success<S, E>(action());
42+
} catch (e, s) {
43+
return Error<S, E>(onError(e, s));
44+
}
45+
}
46+
2347
/// Gets the success value if this result is a [Success].
2448
///
2549
/// Throws [SuccessResultNotFoundException] if this result is an [Error].
@@ -109,6 +133,37 @@ sealed class Result<S, E> {
109133
///
110134
/// This method is useful for chaining operations that might fail without nesting results.
111135
Result<U, E> flatMap<U>(Result<U, E> Function(S success) mapper);
136+
137+
/// Returns the success value if this is [Success], otherwise calls [orElse]
138+
/// with the error value and returns the result.
139+
S getOrElse(S Function(E error) orElse);
140+
141+
/// Executes [action] as a side effect if this is a [Success], then returns
142+
/// this result unchanged. Useful for logging or other side effects in a chain.
143+
Result<S, E> onSuccess(void Function(S success) action);
144+
145+
/// Executes [action] as a side effect if this is an [Error], then returns
146+
/// this result unchanged. Useful for logging or other side effects in a chain.
147+
Result<S, E> onError(void Function(E error) action);
148+
149+
/// Transforms the error value of this result to another result.
150+
///
151+
/// If this result is an [Error], applies [mapper] to the error to produce a
152+
/// new result. If this result is a [Success], returns a new success result
153+
/// carrying the same success value.
154+
///
155+
/// This is the symmetric counterpart of [flatMap] for the error channel.
156+
Result<S, F> flatMapError<F>(Result<S, F> Function(E error) mapper);
157+
158+
/// Alias for [flatMapError]. Recovers from an error by producing a new result.
159+
Result<S, F> recover<F>(Result<S, F> Function(E error) mapper) =>
160+
flatMapError(mapper);
161+
162+
/// Returns a new result with [Success] and [Error] swapped.
163+
///
164+
/// A [Success] value becomes an [Error], and an [Error] value becomes a
165+
/// [Success].
166+
Result<E, S> swap();
112167
}
113168

114169
/// Success Result.
@@ -196,6 +251,26 @@ final class Success<S, E> extends Result<S, E> {
196251
Result<U, E> flatMap<U>(Result<U, E> Function(S success) mapper) {
197252
return mapper(_success);
198253
}
254+
255+
@override
256+
S getOrElse(S Function(E error) orElse) => _success;
257+
258+
@override
259+
Result<S, E> onSuccess(void Function(S success) action) {
260+
action(_success);
261+
return this;
262+
}
263+
264+
@override
265+
Result<S, E> onError(void Function(E error) action) => this;
266+
267+
@override
268+
Result<S, F> flatMapError<F>(Result<S, F> Function(E error) mapper) {
269+
return Success<S, F>(_success);
270+
}
271+
272+
@override
273+
Result<E, S> swap() => Error<E, S>(_success);
199274
}
200275

201276
/// Error Result.
@@ -238,7 +313,7 @@ final class Error<S, E> extends Result<S, E> {
238313
whenError(_error);
239314

240315
@override
241-
S getOrThrow() => throw SuccessResultNotFoundException();
316+
S getOrThrow() => throw SuccessResultNotFoundException<S, E>();
242317

243318
@override
244319
E tryGetError() => _error;
@@ -277,6 +352,26 @@ final class Error<S, E> extends Result<S, E> {
277352
Result<U, E> flatMap<U>(Result<U, E> Function(S success) mapper) {
278353
return Error<U, E>(_error);
279354
}
355+
356+
@override
357+
S getOrElse(S Function(E error) orElse) => orElse(_error);
358+
359+
@override
360+
Result<S, E> onSuccess(void Function(S success) action) => this;
361+
362+
@override
363+
Result<S, E> onError(void Function(E error) action) {
364+
action(_error);
365+
return this;
366+
}
367+
368+
@override
369+
Result<S, F> flatMapError<F>(Result<S, F> Function(E error) mapper) {
370+
return mapper(_error);
371+
}
372+
373+
@override
374+
Result<E, S> swap() => Success<E, S>(_error);
280375
}
281376

282377
/// Exception thrown when attempting to access a success value that doesn't exist.

0 commit comments

Comments
 (0)