-
Notifications
You must be signed in to change notification settings - Fork 12
mapError method and mapCatching errorTransform parameter #8
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
Conversation
* @returns a new {@linkcode AsyncResult} instance with the transformed value | ||
*/ | ||
mapCatching<ReturnType>(transform: (value: Value) => ReturnType) { | ||
mapCatching<ReturnType, ErrorType extends AnyValue>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Atm, we're not doing anything with the ErrorType
in the return type of mapCatching
.
We can fallback to NativeError
when the error transform-fn is not being used, and use the generic ErrorType
in the return type of mapCatching
:
mapCatching<ReturnType, ErrorType extends AnyValue>( | |
mapCatching<ReturnType, ErrorType extends AnyValue = NativeError>( |
.catch((error: unknown) => | ||
resolve(Result.error(errorTransform ? errorTransform(error) : error)), | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should probably wrap the body of the catch
in an additional try-catch, because the errorTransform
function might throw, and atm those exceptions get swallowed. Instead, I think the caught expection should be passed to the missing reject
callback of the AsyncResult
constructor.
Something along the lines of:
try {
resolve(
Result.error(transformError ? transformError(error) : error),
);
} catch (err) {
reject(err);
}
); | ||
}) as ReturnType extends Promise<infer PromiseValue> | ||
? PromiseValue extends Result<infer ResultValue, infer ResultError> | ||
? AsyncResult<ResultValue, Err | ResultError | NativeError> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion to replace all ocurrences of NativeError
in this return type with ErrorType
* if the transform function is async. | ||
*/ | ||
mapCatching<ReturnType>(transform: (value: Value) => ReturnType) { | ||
mapCatching<ReturnType, ErrorType extends AnyValue>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the same comments about the async-variant apply here as well
this.success | ||
? Result.try( | ||
() => transform(this._value), | ||
(error: any) => (errorTransform ? errorTransform(error) : error), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I'm correct, you can pass the reference to errorTransform
fn as is, because they both have the same signature.
* in a failed result. | ||
* | ||
* @param transform callback function to transform the value of the result. The callback can be async as well. | ||
* @param errorTransform optional callback function to transform the value of the error. The callback can be async as well. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we should support an async errorTransform
(at least for now), and I don't think this implementation supports it either.
}); | ||
}); | ||
|
||
describe("mapError", () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see that you use CustomError
a lot in the added test cases. At first glance there is nothing particularly wrong with this, although type-wise it tend to 'slip through the cracks' here and there. Are you ok with using the ErrorA
and ErrorB
classes to proof things like transformations of errors, etc? As bonus you only have to match against the constructor of the error.
throw new Error("TEST_ERROR"); | ||
}, | ||
(_error) => { | ||
return new CustomError(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an example of where CustomError
creates a false positive.
If you replace this with ErrorA
it will probably fail the typecheck. See comment about types of mapCatching
in result.ts
.
Result.assertError(result); | ||
expect(result.error).toBeInstanceOf(CustomError); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add a test for when an exception gets thrown inside the errorTransform
fn?
expect(result.error).toBeInstanceOf(CustomError); | ||
}); | ||
|
||
it("transforms thrown errors when an errorTransform function is defined", async () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the same comments of the sync-variant of mapCatching
apply here as well
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for your work, I appreciate it!! 👍🏻
I think in general it looks good. I have a couple of comments, and one bigger concern in general: are there any practical use cases where it makes sense for mapError
to accept anything other than a function that returns only another error? I notice that in this PR mapError
also accepts the transform fn to be async and/or return another Result
, and I'm not sure about that. I could be overlooking something, so please correct me, but in general I like to restrict the API as much as possible, with the reasoning that it is always easier to relax it later on, instead of vice versa.
Let me know what your thoughts are, or if you need any help.
Yeah, it could be simplified. I was just going to describe a particular use case I have/had. The fetch API returns a Response object that can be checked for an ok-ish response (2xx status code) by checking the response.ok boolean. In case of an ok response, I expect response.data to be valid JSON. In case of a 500 error, it could be valid JSON but it could be some raw error message from whatever failed. So in the mapError I have to do a response.json() which is async and could throw as well. However, I think that could also be handled with a map because map can return a Result. Something like return await Result.try(
() => fetch(url, options),
(error) => MyApplicationError.from(error, url),
).map(async (response) =>
response.ok ? Result.ok(await response.json()) : Result.error(await handleErrorResponse(response)),
) handleErrorResponse does await response.json() and if that fails it does await response.text(). It is So, long story short, I do have errors that need some async work but it isn't really error mapping, more error construction that can be done in a regular map |
On the other notes, thanks for the feedback, this is a nice way to dive into the typescript typesystem. Will go through the feedback whenever I have time this week. |
I think it's an interesting case you got there. I will give it some further thought, but for now, I agree with your conclusion -> differentiate between error mapping and construction. I already had a branch locally a couple of days ago that contained some drafts regarding mapError and I couldn't resist working on it some more tonight, so essentially, I just pushed what I've got and added the things that I discussed in the comments (#9). |
Yeah go for it, don't worry, it was a good practice session for me. Learned a lot (and switching to biome for formatting right now) |
Ok, that's good to hear! Thanks again for your help and your input. Don't hesitate to submit another issue if you need help or have any suggestions to improve this lib. |
No description provided.