Idiomatic promise handling? #551
-
Coming from Rust to Typescript I was very happy to find this library. I am having a hard time finding an idiomatic way to combine usage of the Result type and Promises. E.g. I have a function which necessarily has to be async, is it more idiomatic to use the Promise's native error handling? I want to treat the result as a monad, but having them nested promise monad feels like I'm working against something, which tells me I'm probably using it wrong 😄 Below is an example of an async work function and a consumer of the function which shouldn't return a result, but necessarily has to be a promise. async function someAsyncCall(): Promise<string> {
throw new Error("how does this get handled alongside the Result.Err?");
}
async function tryWork() -> Result<string, Error> {
try {
const workResult: string = await someAsyncCall();
return Result.ok(`work '${workResult}' was done`);
} catch (e) {
return Result.err(e);
}
}
async function maybeWorkDone() -> Maybe<string>
try {
const result = (await tryWork()).unwrapOrElse((e) -> throw e);
return Maybe.of(result);
} catch (e) {
console.error('encountered error working`, e);
return Maybe.nothing();
}
} The issue to be clear is combining the failure modes of the async failing via |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment
-
Yeah, this is an unfortunate reality of working with Ultimately, I still think a In the specific example you offer here, there are a couple things to notice:
Putting those three things together, here's how I might rewrite your example: function tryWork(): Promise<Result<string, Error>> {
return someAsyncCall().then(
(value) => Result.ok(value),
(cause) => {
let error =
cause instanceof Error
? cause
: new Error("failed async call", { cause });
return Result.err(error);
}
);
}
async function maybeWorkDone(): Promise<Maybe<string>> {
let result = await tryWork();
return result.match({
Ok: (value) => Maybe.just(value),
Err: (e) => {
console.error("encountered error working", e);
return Maybe.nothing();
},
});
} A couple additional things to notice here:
Finally, if you want to handle this generically, you can write something like this: function id<A>(a: A) {
return a;
}
function asyncTryOrElse<T>(fn: () => Promise<T>): Promise<Result<T, unknown>>;
function asyncTryOrElse<T, E>(
fn: () => Promise<T>,
onErr: (value: unknown) => E
): Promise<Result<T, E>>;
function asyncTryOrElse<T, E>(
fn: () => Promise<T>,
onErr?: (value: unknown) => E
): Promise<Result<T, unknown>> {
return fn().then(Result.ok<T, E>, onErr ?? id);
} This does basically exactly what the more specific function you were working with does, but does it fully generically. Any (We might think about adding this to the library! It's high-utility and low-cost!) Here's a TS playground demoing all of this! |
Beta Was this translation helpful? Give feedback.
Yeah, this is an unfortunate reality of working with
Promise
in general: its lack of clear handling on the error case itself is really frustrating, but falls out organically from the way that.catch()
was designed to interoperate withthrow
. I wrote up some related notes over on #430, but those don't really get at the thing you're describing here.Ultimately, I still think a
Task
-like API (#25, #215) would be the "right" way to handle this, but it's not trivial to get right and neither @bmakuh nor I have had time to fully tackle that.In the specific example you offer here, there are a couple things to notice:
First, the return type from an
async
function must bePromise<T>
in TypeScrip…