Skip to content

Commit 4fb57b5

Browse files
committed
adds overload to Result.fromAsync to create an async-result using an async callback fn
1 parent 4bb2b18 commit 4fb57b5

File tree

4 files changed

+247
-52
lines changed

4 files changed

+247
-52
lines changed

readme.md

Lines changed: 78 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -685,16 +685,76 @@ See [Async support](#async-support) for more context.
685685

686686
Because it can be quite cumbersome to work with results that are wrapped in a promise, we provide an `AsyncResult` type that is essentially a regular promise that contains a `Result` type, along with most of the methods that are available on the regular `Result` type. This makes it easier to chain operations without having to assign the intermediate results to a variable or having to use `await` for each async operation.
687687

688-
There are of course plenty of scenarios where an async function returns a `Result` (`Promise<Result<*, *>>`). In these cases, you can use the `fromAsync` and `fromAsyncCatching` methods to convert the promise to an `AsyncResult`, and continue chaining operations:
688+
There are of course plenty of scenarios where an async function (or method) returns a `Result` (`Promise<Result<*, *>>`). Although there nothing wrong with this per se, it can become a bit cumbersome to await each function call before you can perform any operations. You can use the `fromAsync` and `fromAsyncCatching` utility methods to make working with results in an async context more ergonomic and developer-friendly.
689+
690+
There are two approaches you can choose from: transform to an `AsyncResult` _directly at the source_, or let the _consuming code_ handle the conversion. Let's look at both approaches in more detail.
691+
692+
#### Transforming to an `AsyncResult` directly at the source
693+
694+
Before:
695+
```ts
696+
// Note the `async` keyword and that we return a `Promise` holding a `Result`
697+
async function findUserById(id: string): Promise<Result<User, NotFoundError>> {
698+
const user = await db.query("SELECT * FROM users WHERE id = ?", [id]);
699+
700+
if (!user) {
701+
return Result.error(new NotFoundError("User not found"));
702+
}
703+
704+
return Result.ok(user);
705+
}
706+
707+
async function getDisplayName(userId: string): Promise<string> {
708+
return (await findUserById(userId))
709+
.map((user) => user.name)
710+
.getOrElse(() => "Unknown User");
711+
}
712+
```
713+
714+
After:
715+
```ts
716+
// Note that we no longer use the `async` keyword and that we return an `AsyncResult`
717+
// instead of a `Promise`
718+
function findUserById(id: string): AsyncResult<User, NotFoundError> {
719+
return Result.fromAsync(async () => {
720+
const user = await db.query("SELECT * FROM users WHERE id = ?", [id]);
721+
722+
if (!user) {
723+
return Result.error(new NotFoundError("User not found"));
724+
}
725+
return Result.ok(user);
726+
});
727+
}
728+
729+
function getDisplayName(userId: string): Promise<string> {
730+
return findUserById(userId)
731+
.map((user) => user.name)
732+
.getOrElse(() => "Unknown User");
733+
}
734+
```
735+
736+
The difference might be subtle, but if your codebase has a lot of async operations it might be worth considering this approach.
737+
738+
#### Let the consuming code handle the conversion
739+
740+
You can also choose to do the conversion from the consuming code. The previous example with this approach would translate to this:
689741

690742
```ts
691-
async function someAsyncOperation(): Promise<Result<number, Error>> {
692-
return Result.ok(42);
743+
async function findUserById(id: string): Promise<Result<User, NotFoundError>> {
744+
const user = await db.query("SELECT * FROM users WHERE id = ?", [id]);
745+
746+
if (!user) {
747+
return Result.error(new NotFoundError("User not found"));
748+
}
749+
750+
return Result.ok(user);
693751
}
694752

695-
const result = await Result.fromAsync(someAsyncOperation())
696-
.map((value) => value * 2)
697-
// etc...
753+
async function getDisplayName(userId: string): Promise<string> {
754+
return Result.fromAsync(findUserById(userId))
755+
.map((user) => user.name)
756+
.getOrElse(() => "Unknown User");
757+
}
698758
```
699759

700760
### Merging or combining results
@@ -743,8 +803,8 @@ const result = Result.all(...tasks.map(createTask)); // Result<Task[], IOError>
743803
- [Result.allCatching(items)](#resultallcatchingitems)
744804
- [Result.wrap(fn)](#resultwrapfn)
745805
- [Result.try(fn, [transform])](#resulttryfn-transform)
746-
- [Result.fromAsync(promise)](#resultfromasyncpromise)
747-
- [Result.fromAsyncCatching(promise)](#resultfromasynccatchingpromise)
806+
- [Result.fromAsync()](#resultfromasync)
807+
- [Result.fromAsyncCatching()](#resultfromasynccatching)
748808
- [Result.assertOk(result)](#resultassertokresult)
749809
- [Result.assertError(result)](#resultasserterrorresult)
750810
- [AsyncResult](#asyncresult)
@@ -1336,15 +1396,19 @@ const result = Result.try(
13361396
); // Result<void, IOError>
13371397
```
13381398

1339-
### Result.fromAsync(promise)
1399+
### Result.fromAsync()
13401400

1341-
Utility method to transform a Promise, that holds a literal value or
1342-
a [`Result`](#result) or [`AsyncResult`](#asyncresult) instance, into an [`AsyncResult`](#asyncresult) instance. Useful when you want to immediately chain operations
1401+
Utility method to:
1402+
- transform a Promise, that holds a literal value or
1403+
a [`Result`](#result) or [`AsyncResult`](#asyncresult) instance; or,
1404+
- transform an async function
1405+
into an [`AsyncResult`](#asyncresult) instance. Useful when you want to immediately chain operations
13431406
after calling an async function.
13441407

13451408
#### Parameters
13461409

1347-
- `promise` a Promise that holds a literal value or a [`Result`](#result) or [`AsyncResult`](#asyncresult) instance.
1410+
- `promise` a Promise that holds a literal value or a [`Result`](#result) or [`AsyncResult`](#asyncresult) instance. or,
1411+
- `fn` an async callback funtion returning a literal value or a [`Result`](#result) or [`AsyncResult`](#asyncresult) instance.
13481412

13491413
**returns** a new [`AsyncResult`](#asyncresult) instance.
13501414

@@ -1366,9 +1430,9 @@ const result = (await someAsyncOperation()).map((value) => value 2); // Result<n
13661430
const asyncResult = Result.fromAsync(someAsyncOperation()).map((value) => value 2); // AsyncResult<number, Error>
13671431
```
13681432

1369-
### Result.fromAsyncCatching(promise)
1433+
### Result.fromAsyncCatching()
13701434

1371-
Similar to [`Result.fromAsync`](#resultfromasyncpromise) this method transforms a Promise into an [`AsyncResult`](#asyncresult) instance.
1435+
Similar to [`Result.fromAsync`](#resultfromasync) this method transforms a Promise or async callback function into an [`AsyncResult`](#asyncresult) instance.
13721436
In addition, it catches any exceptions that might be thrown during the operation and encapsulates them in a failed result.
13731437

13741438
### Result.assertOk(result)

src/integration.test.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -82,15 +82,17 @@ describe("User management app", () => {
8282
this.users[user.id] = user;
8383
}
8484

85-
async findById(id: number) {
86-
const possibleUser = this.users[id];
87-
if (!possibleUser) {
88-
return Result.error(
89-
new NotFoundError(`Cannot find user with id ${id}`),
90-
);
91-
}
92-
93-
return Result.ok(possibleUser);
85+
findById(id: number) {
86+
return Result.fromAsync(async () => {
87+
const possibleUser = this.users[id];
88+
if (!possibleUser) {
89+
return Result.error(
90+
new NotFoundError(`Cannot find user with id ${id}`),
91+
);
92+
}
93+
94+
return Result.ok(possibleUser);
95+
});
9496
}
9597

9698
async existsByEmail(email: string) {
@@ -112,7 +114,8 @@ describe("User management app", () => {
112114
}
113115

114116
updateUserEmail(id: number, email: string) {
115-
return Result.fromAsync(this.userRepository.findById(id))
117+
return this.userRepository
118+
.findById(id)
116119
.map((user) => user.updateEmail(email))
117120
.onSuccess((user) => this.userRepository.save(user))
118121
.map(UserDto.fromUser);

src/result.test.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,55 @@ describe("Result", () => {
592592
Result.fromAsync(myFunction()).map(() => 12),
593593
).rejects.toThrow(CustomError);
594594
});
595+
596+
it("takes an async function and turns it into an async-result", async () => {
597+
const result = Result.fromAsync(async () => {
598+
await sleep();
599+
return Result.ok(12);
600+
});
601+
602+
expect(result).toBeInstanceOf(AsyncResult);
603+
expectTypeOf(result).toEqualTypeOf<AsyncResult<number, never>>();
604+
expect(await result).toEqual(Result.ok(12));
605+
});
606+
607+
it("takes an async function that possibly returns multiple types", async () => {
608+
const exec = (value: number) =>
609+
Result.fromAsync(async () => {
610+
if (value === 1) {
611+
return "one" as const;
612+
}
613+
614+
if (value === 2) {
615+
return Result.ok("two" as const);
616+
}
617+
618+
if (value === 3) {
619+
return AsyncResult.ok("three" as const);
620+
}
621+
622+
if (value === 4) {
623+
return Promise.resolve("four" as const);
624+
}
625+
626+
if (value === 5) {
627+
return Result.error(new ErrorA("five"));
628+
}
629+
630+
return Promise.resolve(Result.error(new ErrorB()));
631+
});
632+
633+
expectTypeOf(exec).returns.toEqualTypeOf<
634+
AsyncResult<"one" | "two" | "three" | "four", ErrorA | ErrorB>
635+
>();
636+
637+
expect(await exec(1)).toEqual(Result.ok("one"));
638+
expect(await exec(2)).toEqual(Result.ok("two"));
639+
expect(await exec(3)).toEqual(Result.ok("three"));
640+
expect(await exec(4)).toEqual(Result.ok("four"));
641+
expect(await exec(5)).toEqual(Result.error(new ErrorA("five")));
642+
expect(await exec(6)).toEqual(Result.error(new ErrorB()));
643+
});
595644
});
596645

597646
describe("Result.fromAsyncCatching", () => {
@@ -618,6 +667,57 @@ describe("Result", () => {
618667
Result.assertError(resolvedAsyncResult);
619668
expect(resolvedAsyncResult.error).toBeInstanceOf(CustomError);
620669
});
670+
671+
it("takes an async function that possibly returns multiple types", async () => {
672+
const exec = (value: number) =>
673+
Result.fromAsyncCatching(async () => {
674+
if (value === 1) {
675+
return "one" as const;
676+
}
677+
678+
if (value === 2) {
679+
return Result.ok("two" as const);
680+
}
681+
682+
if (value === 3) {
683+
return AsyncResult.ok("three" as const);
684+
}
685+
686+
if (value === 4) {
687+
return Promise.resolve("four" as const);
688+
}
689+
690+
if (value === 5) {
691+
return Result.error(new ErrorA("five"));
692+
}
693+
694+
return Promise.resolve(Result.error(new ErrorB()));
695+
});
696+
697+
expectTypeOf(exec).returns.toEqualTypeOf<
698+
AsyncResult<"one" | "two" | "three" | "four", ErrorA | ErrorB | Error>
699+
>();
700+
701+
expect(await exec(1)).toEqual(Result.ok("one"));
702+
expect(await exec(2)).toEqual(Result.ok("two"));
703+
expect(await exec(3)).toEqual(Result.ok("three"));
704+
expect(await exec(4)).toEqual(Result.ok("four"));
705+
expect(await exec(5)).toEqual(Result.error(new ErrorA("five")));
706+
expect(await exec(6)).toEqual(Result.error(new ErrorB()));
707+
});
708+
709+
it("catches thrown exceptions inside the callback correctly", async () => {
710+
const asyncResult = Result.fromAsyncCatching(async () => {
711+
throw new CustomError("Boom!");
712+
});
713+
714+
expectTypeOf(asyncResult).toEqualTypeOf<AsyncResult<never, Error>>();
715+
expect(asyncResult).toBeInstanceOf(AsyncResult);
716+
717+
const result = await asyncResult;
718+
Result.assertError(result);
719+
expect(result.error).toBeInstanceOf(CustomError);
720+
});
621721
});
622722

623723
describe("instance methods and getters", () => {

src/result.ts

Lines changed: 56 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1665,6 +1665,41 @@ export class Result<Value, Err> {
16651665
}
16661666
}
16671667

1668+
/**
1669+
* Utility method to transform an async function to an {@linkcode AsyncResult} instance. Useful when you want to
1670+
* immediately chain operations after calling an async function/method that returns a Result.
1671+
*
1672+
* @param fn the async callback function that returns a literal value or a {@linkcode Result} or {@linkcode AsyncResult} instance.
1673+
*
1674+
* @returns a new {@linkcode AsyncResult} instance.
1675+
*
1676+
* > [!NOTE]
1677+
* > Any exceptions that might be thrown are not caught, so it is your responsibility
1678+
* > to handle these exceptions. Please refer to {@linkcode Result.fromAsyncCatching} for a version that catches exceptions
1679+
* > and encapsulates them in a failed result.
1680+
*
1681+
* @example
1682+
* basic usage
1683+
*
1684+
* ```ts
1685+
* function findUserById(id: string) {
1686+
* return Result.fromAsync(async () => {
1687+
* const user = await db.query("SELECT * FROM users WHERE id = ?", [id]);
1688+
*
1689+
* if (!user) {
1690+
* return Result.error(new NotFoundError("User not found"));
1691+
* }
1692+
*
1693+
* return Result.ok(user);
1694+
* });
1695+
* }
1696+
*
1697+
* const displayName = await findUserById("123").fold((user) => user.name, () => "Unknown User");
1698+
* ```
1699+
*/
1700+
static fromAsync<T>(
1701+
fn: () => Promise<T>,
1702+
): AsyncResult<ExtractValue<T>, ExtractError<T>>;
16681703
/**
16691704
* Utility method to transform a Promise, that holds a literal value or
16701705
* a {@linkcode Result} or {@linkcode AsyncResult} instance, into an {@linkcode AsyncResult} instance. Useful when you want to immediately chain operations
@@ -1692,40 +1727,33 @@ export class Result<Value, Err> {
16921727
* const asyncResult = Result.fromAsync(someAsyncOperation()).map((value) => value * 2); // AsyncResult<number, Error>
16931728
* ```
16941729
*/
1695-
static fromAsync<T extends Promise<AnyAsyncResult>>(
1696-
value: T,
1697-
): T extends Promise<AsyncResult<infer V, infer E>>
1698-
? AsyncResult<V, E>
1699-
: never;
1700-
static fromAsync<T extends Promise<AnyResult>>(
1701-
value: T,
1702-
): T extends Promise<Result<infer V, infer E>> ? AsyncResult<V, E> : never;
1703-
static fromAsync<T extends AnyPromise>(
1704-
value: T,
1705-
): T extends Promise<infer V> ? AsyncResult<V, never> : never;
1706-
static fromAsync(value: unknown): unknown {
1707-
return Result.run(() => value);
1730+
static fromAsync<T>(
1731+
value: Promise<T>,
1732+
): AsyncResult<ExtractValue<T>, ExtractError<T>>;
1733+
static fromAsync(valueOrFn: AnyPromise | AnyAsyncFunction) {
1734+
return Result.run(
1735+
typeof valueOrFn === "function" ? valueOrFn : () => valueOrFn,
1736+
);
17081737
}
17091738

1739+
/**
1740+
* Similar to {@linkcode Result.fromAsync} this method transforms an async callback function into an {@linkcode AsyncResult} instance.
1741+
* In addition, it catches any exceptions that might be thrown during the operation and encapsulates them in a failed result.
1742+
*/
1743+
static fromAsyncCatching<T>(
1744+
fn: () => Promise<T>,
1745+
): AsyncResult<ExtractValue<T>, ExtractError<T> | NativeError>;
17101746
/**
17111747
* Similar to {@linkcode Result.fromAsync} this method transforms a Promise into an {@linkcode AsyncResult} instance.
17121748
* In addition, it catches any exceptions that might be thrown during the operation and encapsulates them in a failed result.
17131749
*/
1714-
static fromAsyncCatching<T extends Promise<AnyAsyncResult>>(
1715-
value: T,
1716-
): T extends Promise<AsyncResult<infer V, infer E>>
1717-
? AsyncResult<V, E | NativeError>
1718-
: never;
1719-
static fromAsyncCatching<T extends Promise<AnyResult>>(
1720-
value: T,
1721-
): T extends Promise<Result<infer V, infer E>>
1722-
? AsyncResult<V, E | NativeError>
1723-
: never;
1724-
static fromAsyncCatching<T extends AnyPromise>(
1725-
value: T,
1726-
): T extends Promise<infer V> ? AsyncResult<V, NativeError> : never;
1727-
static fromAsyncCatching(value: unknown): unknown {
1728-
return Result.try(() => value);
1750+
static fromAsyncCatching<T>(
1751+
value: Promise<T>,
1752+
): AsyncResult<ExtractValue<T>, ExtractError<T> | NativeError>;
1753+
static fromAsyncCatching(valueOrFn: AnyPromise | AnyAsyncFunction) {
1754+
return Result.try(
1755+
typeof valueOrFn === "function" ? valueOrFn : () => valueOrFn,
1756+
);
17291757
}
17301758

17311759
/**

0 commit comments

Comments
 (0)