-
Notifications
You must be signed in to change notification settings - Fork 135
feat: Add andFinally functionality and tests #588
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| 'neverthrow': minor | ||
| --- | ||
|
|
||
| Add `andFinally` to allow for synchronous or asynchronous cleanup code that will always be called, and only propagates errors. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -179,6 +179,23 @@ export class ResultAsync<T, E> implements PromiseLike<Result<T, E>> { | |
| ) | ||
| } | ||
|
|
||
| andFinally<F>(f: () => ResultAsync<unknown, F> | Result<unknown, F>): ResultAsync<T, E | F> { | ||
| return new ResultAsync( | ||
| this._promise.then( | ||
| async (res) => { | ||
| const cleanupResult = await f() | ||
| return res.andFinally(() => cleanupResult) | ||
| }, | ||
| async (err) => { | ||
| // this should be unreachable for well-behaved ResultAsync instances, but let's run the | ||
| // cleanup and propagate the erroneous rejection anyway | ||
| await f() | ||
| throw err | ||
|
Comment on lines
+190
to
+193
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See the PR description for more details about why this was added. Open to hearing concerns about this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hello @macksal Nice PR. I am facing the same issue so I came here. It would be great if maintainers gave some attention to this PR. The only problem I see, is that the finalization function Unfortunately, there is no way around but to create some sort of combined error that will return both original and finalization errors. Here is how I implemented it in my code: import { ResultAsync } from 'neverthrow';
import { Throwable } from './throwable';
export class CombinedError<PE, FE> extends Throwable {
public readonly _tag = 'CombinedError';
constructor(
public readonly programError: PE,
public readonly finalizationError: FE,
) {
super('Both program and finalization have failed');
}
}
export function andFinally<R, PE, FE>(
program: ResultAsync<R, PE>,
finalization: () => ResultAsync<void, FE>,
): ResultAsync<R, PE | FE | CombinedError<PE, FE>> {
return ResultAsync.fromSafePromise(
new Promise((resolve, reject) => {
program.match(
async (result) => {
await finalization().match(
() => resolve(result),
(finalizationError: FE) => reject(finalizationError),
);
},
async (error) => {
await finalization().match(
() => reject(error),
(finalizationError: FE) => reject(new CombinedError(error, finalizationError)),
);
},
);
}),
);
}There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @hosuaby I did this to mirror the precedent of throwing inside finally blocks - most languages do not special case this and simply "overwrite" the previously thrown error if there was one. By extension if our andFinally cleanup fails, we will return that error and ignore the error from the existing result (if there is one). In my eyes the finally use case is a tool for when you want to decouple some cleanup behaviour from the result of everything before it ("just make sure we run this regardless of what happens beforehand") Therefore if you find yourself wanting to reintroduce that coupling by inspecting the combined error types, those behaviours are probably more related than you thought. You can easily solve this by making your steps+cleanup in the happy/sad paths more explicit. Whatever we do in the library here would be right for some consumers and wrong for others IMO, so let's go with what is most simple |
||
| }, | ||
| ), | ||
| ) | ||
| } | ||
|
|
||
| orElse<R extends Result<unknown, unknown>>( | ||
| f: (e: E) => R, | ||
| ): ResultAsync<InferOkTypes<R> | T, InferErrTypes<R>> | ||
|
|
||
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.
Allowing for either sync or async Results to be returned by the callback. I couldn't foresee any problems with this and it makes the method more flexible.