Async operations
Existing Result implementations often face criticism for poor asynchronous operation support. The example below demonstrates how quickly things can become unwieldy:
async function operationA() {
return Math.random() > 0.5
? Result.ok("Operation A succeeded")
: Result.error(new ErrorA("Operation A failed"));
}
async function operationB() {
return Math.random() > 0.5
? Result.ok("Operation B succeeded")
: Result.error(new ErrorB("Operation B failed"));
}
const result = await (await operationA()).map(async () => {
/* some logic here */
return (await operationB()).map(() => "Final result");
});
Luckily, TypeScript Result offers first class support for asynchronous operations.
A box within a box
In JavaScript/TypeScript, an async function always returns a Promise
. If that function happens to return a Result
, it will return a Promise<Result<...>>
, hence the term "box within a box": we need to unwrap two layers in order to get to the actual value or error. As can be seen in the previous example, this is not very ergonomic and quickly leads to unreadable code.
One box to rule them all
To solve this problem, TypeScript Result has the AsyncResult
type.
Instead of two nested 'boxes', we combine the
Promise
andResult
into a single type:AsyncResult
.
INFO
This is really the case under the hood: AsyncResult
is a Promise
that holds a Result
, along with the same methods (e.g. map
, toTuple
, etc.) a regular Result
has.
Result.fromAsync
Most operations on the Result instance already automatically turn async operations into AsyncResult
instances, so you don't have to worry about it:
declare const result: Result<string, Error>;
const nextResult = result.map(async (value) => {
await sleep(1000); // Simulating an async operation
return value.toUpperCase();
}): // AsyncResult<string, Error>
However, there's one use case that is impossible to handle automatically: functions returning a Promise<Result<...>>
. This is where the Result.fromAsync
method comes in handy.
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.
function operationA() {
return Result.fromAsync(async () =>
Math.random() > 0.5
? Result.ok("Operation A succeeded")
: Result.error(new ErrorA("Operation A failed"))
);
}
function operationB() {
return Result.fromAsync(async () =>
Math.random() > 0.5
? Result.ok("Operation B succeeded")
: Result.error(new ErrorB("Operation B failed"))
);
}
const result = await operationA().map(() => {
/* some logic here */
return operationB().map(() => "Final result");
});
async function operationA() {
Math.random() > 0.5
? Result.ok("Operation A succeeded")
: Result.error(new ErrorA("Operation A failed"))
}
async function operationB() {
Math.random() > 0.5
? Result.ok("Operation B succeeded")
: Result.error(new ErrorB("Operation B failed"))
}
const result = await Result.fromAsync(operationA()).map(() => {
/* some logic here */
return Result.fromAsync(operationB()).map(() => "Final result");
});
Both approaches work. We recommend to pick the 'directly at the source' approach, mainly because it plays nicer in conjunction with generators:
const result = Result.gen(function* () {
yield* operationA();
/* some logic here */
yield* operationB();
return "Final result";
});