-
Notifications
You must be signed in to change notification settings - Fork 88
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feat request: fromAsync to interop with async functions more easily #608
Comments
Hello @tmcw ! Thank you for this proposal. Could you provide an example of what you resort to doing with the current API ? |
Sure, here's a (genericized) example from our codebase - in this example and most others, we use Mostly because I still want to use async/await, so using an async function makes sense, but async functions always return a promise-wrapped value. export async function getItem({
currentUser,
}: {
currentUser: CurrentUser;
}): Promise<
Result<
{
id: string | null;
},
HttpError
>
> {
if (!currentUser) {
return err(httpErrors.unauthorized("Unauthorized"));
}
// Check if the currentUser has access to this evaluation Id
const [item] = await db
.select({
id: items.id,
})
.from(items);
if (!item) {
return err(httpErrors.notFound("Item not found"));
}
return ok({
id: item.id,
});
} |
hey @tmcw ! on the other hand when I see the return type is function getItem({
currentUser,
}: {
currentUser: CurrentUser | null
}): ResultAsync<{ id: string }, HttpError> {
// Check if the currentUser has access to this evaluation Id
if (!currentUser) {
return errAsync(httpErrors.unauthorized('Unauthorized'))
}
return ResultAsync.fromThrowable(
async () => db.select({ id: items.id }).from(items),
(err) => httpErrors.internal(err),
)().andThen((foundItems) => {
if (foundItems.length === 0) {
return errAsync(httpErrors.notFound('Item not found'))
}
const [item] = foundItems
return okAsync({ id: item.id })
})
}
async function main(): Promise<void> {
await getItem({ currentUser: { name: 'John Doe' } }).match(
({ id }) => console.log(`Item found with id: ${id}`),
(error) => console.log(`got an HttpError: ${error.message}`),
)
}
main() |
Sorry, maybe my example wasn't explicit enough - the The main thing that differs here is that it would let me use |
oh, now I see what you mean :) yeah, it's a pity that typescript does not allow us to return custom promise implementation in async functions... what I did for such cases is wrote a helper module that looks like this import { Err, Ok, Result, ResultAsync, err, ok } from "neverthrow";
type ResultOrOkValue<T, E> =
| T
| Ok<T, E>
| Err<T, E>
| Ok<ResultOrOkValue<T, E>, E>;
type EitherResult<T, E> = Result<T, E> | ResultAsync<T, E>;
type EitherResultOrOkValue<T, E> = ResultAsync<T, E> | ResultOrOkValue<T, E>;
type NestedEitherResult<T, E> = EitherResult<EitherResultOrOkValue<T, E>, E>;
type Next = [1, 2, 3, 4, 5, ...never[]];
type InferNestedOkType<T, E, Depth extends number = 0> = Depth extends never
? never
: T extends EitherResult<infer U, E>
? InferNestedOkType<U, E, Next[Depth]>
: T;
export function isResult<T, E>(
maybeResult: EitherResult<T, E> | T | E
): maybeResult is Result<T, E> {
return maybeResult instanceof Ok || maybeResult instanceof Err;
}
export function isResultAsync<T, E>(
maybeResult: EitherResult<T, E> | T | E
): maybeResult is ResultAsync<T, E> {
return maybeResult instanceof ResultAsync;
}
export function isEitherResult<T, E>(
maybeResult: EitherResult<T, E> | T | E
): maybeResult is EitherResult<T, E> {
return isResult(maybeResult) || isResultAsync(maybeResult);
}
function flattenAsync<T, E>(
result: NestedEitherResult<T, E>
): ResultAsync<InferNestedOkType<T, E>, E> {
const flattenResults = async (): Promise<
Result<InferNestedOkType<T, E>, E>
> => {
const outerResult = await result;
if (outerResult.isErr()) {
return err(outerResult.error);
}
let outerValue = outerResult.value;
while (isEitherResult(outerValue)) {
// eslint-disable-next-line no-await-in-loop
const inner = await outerValue;
if (inner.isErr()) {
return err(inner.error);
}
outerValue = inner.value;
}
return ok(outerValue as InferNestedOkType<T, E>);
};
return new ResultAsync(flattenResults());
}
export function fromActionAsync<T, E>(
performAction: () => Promise<NestedEitherResult<T, E>>,
makeError: (error: unknown) => E
): ResultAsync<InferNestedOkType<T, E>, E> {
return flattenAsync(ResultAsync.fromPromise(performAction(), makeError));
}
async function main(): Promise<void> {
const getResult = (): Result<number, string> => ok(1);
const getResultAsync = (): ResultAsync<number, string> => okAsync(2);
const asyncAction = async (): Promise<
ResultAsync<Result<number, Error>, Error>
> => {
const first = getResult();
if (first.isErr()) {
return errAsync(new Error(first.error));
}
const second = await getResultAsync();
if (second.isErr()) {
return errAsync(new Error(second.error));
}
return okAsync(ok(1));
};
const res = await fromActionAsync(asyncAction, Error);
if (res.isErr()) {
console.log(`Error: ${res.error}`);
} else {
// typeof res.value === 'number'
console.log(`Value: ${res.value}`); // 1
}
}
main(); |
Yep! Roughly the same idea. I've been updating and using our function, and figured out how to make the second argument, the error mapper, option, so that by default you'll get something like an UnexpectedError or a 'defect' in Effect talk: export function resultFromAsync<
A extends readonly any[],
R extends Promise<Result<unknown, unknown>>
>(
fn: (...args: A) => R
): (
...args: A
) => ResultAsync<
InferOkTypes<Awaited<R>>,
InferErrTypes<Awaited<R>> | UnexpectedError
>;
export function resultFromAsync<
A extends readonly any[],
IO,
IE,
E,
R extends Promise<Result<IO, IE>>
>(
fn: (...args: A) => R,
errorFn: (err: unknown) => E
): (
...args: A
) => ResultAsync<
InferOkTypes<Awaited<R>>,
InferErrTypes<Awaited<R>> | UnexpectedError | E
>;
export function resultFromAsync<
A extends readonly any[],
E,
IO,
IE,
R extends Promise<Result<IO, IE>>
>(fn: (...args: A) => R, errorFn?: (err: unknown) => E) {
return (...args: A) =>
new ResultAsync<IO, IE | E | UnexpectedError>(
fn(...args).catch((error) =>
errorFn
? new Err(errorFn(error))
: new Err(
new UnexpectedError("Unexpected error (generic)", {
cause: error,
})
)
)
);
} |
Hi! I've been using neverthrow and it's been great! Really enjoying it. I would like to propose 'one more API' 😆 :
The main thing is that we have a bunch of async functions that have known, discrete error types. For example, something that queries the database and if it doesn't find that thing, it returns an error state for the 404. But this means that technically there are two error states - the database query fails (unexpectedly, a 'defect' in Effect terminology), or the thing isn't found (a good ol' error state).
I'd love an API like this, which would let you wrap an async function that returns a Result:
It'd also be super nice for this to automatically handle a ResultAsync returned by the async function. This'd give pretty good ergonomics for the common usecase of having an async function with some known error types that you'd like to preserve.
The text was updated successfully, but these errors were encountered: