Why is there no `ResultAsync.fromThrowable`?
ehaynes99 opened this issue · 8 comments
Result.fromThrowable
takes a function as an argument, and returns a new function that returns a Result
. There is no corollary for ResultAsync
, which really diminishes the utility of the library. There have been a lot of issues in here mentioning how people want to have an async
function that returns a ResultAsync
. This is just fundamentally wrong. It defeats the entire purpose of using a library like this, because the whole point is that the errors are moved from thrown, untyped values into the return type of the function.
For example:
// doesn't compile because it's not returning a `Promise`, and thats a GOOD THING
const getOrders = async (username: string): ResultAsync<Order[], AxiosError> => {
// This is bad, because it can throw
const { data } = await axios.get(`/users/username/${username}`)
const { id } = data
return ResultAsync.fromPromise(axios.get(`orders/customer-id/${id}`).then(({ data }) => data), (cause) => cause as AxiosError)
}
To use such a function, you would have to handle the potential error in the result, but you would also still need to wrap it in a try/catch
try {
await getOrders('someUser').match(
(orders: Order[]) => console.log(orders),
// typesafe error type
(error: AxiosError) => res.send(500, { message: error.message }),
)
} catch (error: unknown) {
// no type safety
res.send(500, { message: error instanceof Error ? error.message : 'unknown error' })
}
fromPromise
does not provide sufficient type safety, because it's possible to have synchronous error thrown while creating said promise:
const updateUser = (user: Partial<User>): Promise<User> => {
const { id, ...updates } = user
if (!id || Object.keys(updates).length === 0) {
// since this is not an `async` function, this throws in a synchronous context
throw new TypeError(`invalid user value: ${user}`)
}
return axios.put(`/users/$id`, updates).then(({ data }) => data)
}
// `TypeError` will be thrown here, not captured in the result
const userResult = ResultAsync.fromPromise(updateUser({}), (err: unknown) => err as TypeError | AxiosError)
For ResultAsync
to actually provide safety, it's rather critical IMO to be able to convert an arbitrary function that returns a Promise
to a wrapper enclosed with ResultAsync
that is able to handle both types of errors, not just the rejected Promise
const updateUser = ResultAsync.fromThrowable(
(user: Partial<User>): Promise<User> => {
const { id, ...updates } = user
if (!id || Object.keys(updates).length === 0) {
// wrapper could capture sync errors
throw new TypeError(`invalid user value: ${user}`)
}
// and also async errors from a rejected `Promise`
return axios.put(`/users/$id`, updates).then(({ data }) => data)
},
(err: unknown) => err as AxiosError | TypeError,
)
An implementation might look like this, which would "hoist" any synchronous errors into the async context.
export const fromThrowable = <Fn extends (...args: readonly any[]) => Promise<any>, E>(
fn: Fn,
errorFn: (err: unknown) => E,
): ((...args: Parameters<Fn>) => ResultAsync<ReturnType<Fn> extends Promise<infer R> ? R : never, E>) => {
return (...args) => {
try {
return ResultAsync.fromPromise(fn(...args), errorFn)
} catch (error) {
return ResultAsync.fromPromise(Promise.reject(error), errorFn)
}
}
}
@ehaynes99 Thanks for this ! I think this proposal merits a PR ! Could you do it ?
Better late than never. I wasn't using this library for a while, but I added a PR for this.
Just published now under v6.2.0
:
https://github.com/supermacro/neverthrow/releases/tag/v6.2.0
Broken build on npm. There is no dist directory.
Fixed in v6.2.1 .. sorry about that!