Skip to content

Commit 4ddb95f

Browse files
committed
feat(ResultAsync): add from() error handling, update Result async methods
1 parent 45f4fa5 commit 4ddb95f

9 files changed

Lines changed: 211 additions & 97 deletions

File tree

src/result.ts

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { isSome } from '.';
1+
import { ErrorHandler, isSome } from '.';
22
import { ResultAsync } from './resultAsync';
33
import { Unit } from './unit';
44
import {
@@ -175,7 +175,7 @@ export class Result<TValue = Unit, TError = string> {
175175
*/
176176
static try<TValue, TError = string>(
177177
factory: FunctionOfT<Some<TValue>>,
178-
errorHandler: FunctionOfTtoK<unknown, TError>
178+
errorHandler: ErrorHandler<TError>
179179
): Result<TValue, TError>;
180180
/**
181181
* Creates a new successful Result with a Unit value.
@@ -186,7 +186,7 @@ export class Result<TValue = Unit, TError = string> {
186186
*/
187187
static try<TError = string>(
188188
action: Action,
189-
errorHandler: FunctionOfTtoK<unknown, TError>
189+
errorHandler: ErrorHandler<TError>
190190
): Result<Unit, TError>;
191191
/**
192192
* Creates a new successful Result with the return value
@@ -198,7 +198,7 @@ export class Result<TValue = Unit, TError = string> {
198198
*/
199199
static try<TValue = Unit, TError = string>(
200200
actionOrFactory: FunctionOfT<Some<TValue>> | Action,
201-
errorHandler: FunctionOfTtoK<unknown, Some<TError>>
201+
errorHandler: ErrorHandler<TError>
202202
): Result<TValue, TError> {
203203
try {
204204
const value = actionOrFactory();
@@ -480,27 +480,31 @@ export class Result<TValue = Unit, TError = string> {
480480
/**
481481
* Maps the value successful Result to a new async value wrapped in a ResultAsync
482482
* @param projection a function given the value of the current Result which returns a Promise of some value
483+
* @param errorHandler a function that converts the error of the rejected Promise to a failed Result
483484
* @returns
484485
*/
485486
mapAsync<TNewValue>(
486-
projection: FunctionOfTtoK<TValue, Promise<Some<TNewValue>>>
487+
projection: FunctionOfTtoK<TValue, Promise<Some<TNewValue>>>,
488+
errorHandler?: ErrorHandler<TError>
487489
): ResultAsync<TNewValue, TError> {
488490
return this.isSuccess
489-
? ResultAsync.from(projection(this.getValueOrThrow()))
491+
? ResultAsync.from(projection(this.getValueOrThrow()), errorHandler)
490492
: ResultAsync.failure(this.getErrorOrThrow());
491493
}
492494

493495
/**
494496
* Maps the error of a failed Result to a new async value wrapped in a ResultAsync
495497
* @param projection a function given the error of the current Result which returns a Promise of some value
498+
* @param errorHandler a function that converts the error of the rejected Promise to a failed Result
496499
* @returns
497500
*/
498501
mapFailureAsync(
499-
projection: FunctionOfTtoK<TError, Promise<Some<TValue>>>
502+
projection: FunctionOfTtoK<TError, Promise<Some<TValue>>>,
503+
errorHandler?: ErrorHandler<TError>
500504
): ResultAsync<TValue, TError> {
501505
return this.isSuccess
502506
? ResultAsync.from(this)
503-
: ResultAsync.from(projection(this.getErrorOrThrow()));
507+
: ResultAsync.from(projection(this.getErrorOrThrow()), errorHandler);
504508
}
505509

506510
/**
@@ -519,9 +523,11 @@ export class Result<TValue = Unit, TError = string> {
519523
/**
520524
* Maps a successful Result to a new ResultAsync
521525
* @param projection
526+
* @param errorHandler a function that converts the error of the rejected Promise to a failed Result
522527
*/
523528
bindAsync<TNewValue>(
524-
projection: FunctionOfTtoK<TValue, Promise<Result<TNewValue, TError>>>
529+
projection: FunctionOfTtoK<TValue, Promise<Result<TNewValue, TError>>>,
530+
errorHandler?: ErrorHandler<TError>
525531
): ResultAsync<TNewValue, TError>;
526532
/**
527533
* Maps a successful Result to a new ResultAsync
@@ -533,12 +539,14 @@ export class Result<TValue = Unit, TError = string> {
533539
/**
534540
* Maps a successful Result to a new ResultAsync
535541
* @param projection
542+
* @param errorHandler a function that converts the error of the rejected Promise to a failed Result
536543
* @returns
537544
*/
538545
bindAsync<TNewValue>(
539546
projection:
540547
| FunctionOfTtoK<TValue, Promise<Result<TNewValue, TError>>>
541-
| FunctionOfTtoK<TValue, ResultAsync<TNewValue, TError>>
548+
| FunctionOfTtoK<TValue, ResultAsync<TNewValue, TError>>,
549+
errorHandler?: ErrorHandler<TError>
542550
): ResultAsync<TNewValue, TError> {
543551
if (this.isFailure) {
544552
return ResultAsync.failure(this.getErrorOrThrow());
@@ -547,7 +555,7 @@ export class Result<TValue = Unit, TError = string> {
547555
const resultAsyncOrPromise = projection(this.getValueOrThrow());
548556

549557
return isPromise(resultAsyncOrPromise)
550-
? ResultAsync.from<TNewValue, TError>(resultAsyncOrPromise)
558+
? ResultAsync.from<TNewValue, TError>(resultAsyncOrPromise, errorHandler)
551559
: resultAsyncOrPromise;
552560
}
553561

@@ -580,16 +588,23 @@ export class Result<TValue = Unit, TError = string> {
580588
/**
581589
* Executes an async action if the Result succeeded
582590
* @param action a function given the Result's value returns a Promise
591+
* @param errorHandler a function that converts the error of the rejected Promise to a failed Result
583592
* @returns a ResultAsync
584593
*/
585-
tapAsync(action: AsyncActionOfT<TValue>): ResultAsync<TValue, TError> {
594+
tapAsync(
595+
action: AsyncActionOfT<TValue>,
596+
errorHandler?: ErrorHandler<TError>
597+
): ResultAsync<TValue, TError> {
586598
if (this.isFailure) {
587599
return ResultAsync.failure(this.getErrorOrThrow());
588600
}
589601

590602
const value = this.getValueOrThrow();
591603

592-
return ResultAsync.from(action(value).then(() => value));
604+
return ResultAsync.from(
605+
action(value).then<Some<TValue>>(() => value),
606+
errorHandler
607+
);
593608
}
594609

595610
/**
@@ -686,12 +701,12 @@ export class Result<TValue = Unit, TError = string> {
686701
/**
687702
*
688703
* @param action
689-
* @param errorCreator
704+
* @param errorHandler
690705
* @returns
691706
*/
692707
onSuccessTry(
693708
action: ActionOfT<TValue>,
694-
errorCreator: FunctionOfTtoK<unknown, Some<TError>>
709+
errorHandler: ErrorHandler<TError>
695710
): Result<TValue, TError> {
696711
if (this.isFailure) {
697712
return this;
@@ -704,7 +719,7 @@ export class Result<TValue = Unit, TError = string> {
704719

705720
return Result.success(value);
706721
} catch (error: unknown) {
707-
return Result.failure(errorCreator(error));
722+
return Result.failure(errorHandler(error));
708723
}
709724
}
710725

@@ -716,13 +731,13 @@ export class Result<TValue = Unit, TError = string> {
716731
*/
717732
onSuccessTryAsync(
718733
asyncAction: FunctionOfTtoK<TValue, Promise<void>>,
719-
errorHander: FunctionOfTtoK<unknown, Some<TError>>
734+
errorHander: ErrorHandler<TError>
720735
): ResultAsync<TValue, TError> {
721736
if (this.isFailure) {
722737
return ResultAsync.failure(this.getErrorOrThrow());
723738
}
724739

725-
const result = async () => {
740+
const promiseFactory = async () => {
726741
const value = this.getValueOrThrow();
727742

728743
try {
@@ -734,7 +749,7 @@ export class Result<TValue = Unit, TError = string> {
734749
}
735750
};
736751

737-
return ResultAsync.from<TValue, TError>(result());
752+
return ResultAsync.from<TValue, TError>(promiseFactory(), errorHander);
738753
}
739754

740755
/**

src/resultAsync.ts

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { ErrorHandler } from '.';
12
import { Result } from './result';
23
import { Unit } from './unit';
34
import {
@@ -29,28 +30,42 @@ export class ResultAsync<TValue = Unit, TError = string> {
2930
): ResultAsync<TValue, TError>;
3031
/**
3132
* Creates a new ResultAsync from the given Promise
32-
* @param value a Promise returning a value which will be wrapped in a successful Result
33+
* @param value a Promise resolving to a Result
34+
* @param errorHandler
3335
*/
3436
static from<TValue, TError>(
35-
value: Promise<Some<TValue>>
37+
value: Promise<Result<TValue, TError>>,
38+
errorHandler?: ErrorHandler<TError>
3639
): ResultAsync<TValue, TError>;
3740
/**
3841
* Creates a new ResultAsync from the given Promise
39-
* @param value a Promise returning a Result, if it resolves
42+
* @param value a Promise which will be converted into a successful Result if it resolves
43+
* and a failed Result if it rejects
4044
*/
4145
static from<TValue, TError>(
42-
value: Promise<Result<TValue, TError>>
46+
value: Promise<Some<TValue>>,
47+
errorHandler?: ErrorHandler<TError>
4348
): ResultAsync<TValue, TError>;
49+
4450
static from<TValue, TError>(
4551
value:
46-
| Promise<Some<TValue>>
47-
| Promise<Result<TValue, TError>>
4852
| Result<TValue, TError>
53+
| Promise<Result<TValue, TError>>
54+
| Promise<Some<TValue>>,
55+
errorHandler?: ErrorHandler<TError>
4956
): ResultAsync<TValue, TError> {
5057
if (isPromise(value)) {
51-
return new ResultAsync(
52-
value.then((v) => (v instanceof Result ? v : Result.success(v)))
58+
let promise = value.then((v) =>
59+
v instanceof Result ? v : Result.success<TValue, TError>(v)
5360
);
61+
62+
if (isFunction(errorHandler)) {
63+
promise = promise.catch((error) =>
64+
Result.failure<TValue, TError>(errorHandler(error))
65+
);
66+
}
67+
68+
return new ResultAsync(promise);
5469
} else if (value instanceof Result) {
5570
return new ResultAsync(Promise.resolve(value));
5671
}
@@ -165,7 +180,7 @@ export class ResultAsync<TValue = Unit, TError = string> {
165180
private value: Promise<Result<TValue, TError>>;
166181

167182
protected constructor(value: Promise<Result<TValue, TError>>) {
168-
this.value = value.catch((error) => Result.failure(error));
183+
this.value = value;
169184
}
170185

171186
/**

src/utilities.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ export type Some<T> = T extends None ? never : T;
1111
export type None = null | undefined | void;
1212
export type Known<TValue> = Exclude<unknown, TValue>;
1313

14+
export type ErrorHandler<TError> = FunctionOfTtoK<unknown, Some<TError>>;
15+
1416
export const never: ActionNever = () => {
1517
throw Error('This error should be unreachable');
1618
};

test/result/bindAsync.spec.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,19 @@ describe('Result', () => {
2929
expect(result).toFailWith(error);
3030
expect(wasCalled).toBe(false);
3131
});
32+
33+
test('will call the projection function when the original Result succeeds and convert a rejected Promise to a failed Result', async () => {
34+
const sut = Result.success(1);
35+
36+
const result = await sut
37+
.bindAsync(
38+
(_number) => Promise.reject('reject'),
39+
(e) => (typeof e === 'string' ? e : 'caught')
40+
)
41+
.toPromise();
42+
43+
expect(result).toFailWith('reject');
44+
});
3245
});
3346

3447
describe('ResultAsync', () => {

test/result/mapAsync.spec.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,20 @@ describe('Result', () => {
2727
expect(result).toFailWith(error);
2828
expect(wasCalled).toBe(false);
2929
});
30+
31+
test('will execute the mapping with a failed Result and convert a rejected Promise to a failed Result', async () => {
32+
const sut = Result.success(1);
33+
34+
const result = await sut
35+
.mapAsync(
36+
(_num) => {
37+
return Promise.reject('reject');
38+
},
39+
(e) => (typeof e === 'string' ? e : 'caught')
40+
)
41+
.toPromise();
42+
43+
expect(result).toFailWith('reject');
44+
});
3045
});
3146
});

test/result/mapFailureAsync.spec.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,25 @@ describe('Result', () => {
2828
expect(innerResult).toSucceedWith(2);
2929
expect(wasCalled).toBe(true);
3030
});
31+
32+
test('will execute the projection with a failed Result and convert a rejected Promise to a failed Result', async () => {
33+
const error = 'error';
34+
let wasCalled = false;
35+
const sut = Result.failure<number>(error);
36+
37+
const innerResult = await sut
38+
.mapFailureAsync(
39+
(_error) => {
40+
wasCalled = true;
41+
42+
return Promise.reject('reject');
43+
},
44+
(e) => (typeof e === 'string' ? e : 'caught')
45+
)
46+
.toPromise();
47+
48+
expect(innerResult).toFailWith('reject');
49+
expect(wasCalled).toBe(true);
50+
});
3151
});
3252
});

test/result/tapAsync.spec.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,19 @@ describe('Result', () => {
1717
expect(wasCalled).toBe(true);
1818
});
1919

20-
test('will return a rejected Promise if the Result is successful and the asyncAction rejects', async () => {
20+
test('will return a failed Result if the Result is successful and the asyncAction rejects', async () => {
2121
const sut = Result.success(1);
22+
const error = 'error';
2223

2324
const asyncAction = () => {
24-
return Promise.reject('error');
25+
return Promise.reject(error);
2526
};
2627

27-
const result = await sut.tapAsync(asyncAction).toPromise();
28+
const innerResult = await sut
29+
.tapAsync(asyncAction, (e) => (typeof e === 'string' ? e : 'caught'))
30+
.toPromise();
2831

29-
expect(result).toFailWith('error');
32+
expect(innerResult).toFailWith(error);
3033
});
3134

3235
test('will not execute the asynchronous action if the Result is a failure', async () => {

0 commit comments

Comments
 (0)