Skip to content

Commit 7b71911

Browse files
w01fgangclaude
andcommitted
fix: per-instance finally callbacks on shared exec (R-02)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c539b25 commit 7b71911

1 file changed

Lines changed: 26 additions & 8 deletions

File tree

src/core/Try.ts

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,12 @@ export class Try<
8484
result?: TryResult<TReturn>;
8585
promise?: Promise<TryResult<TReturn>>;
8686
isAsync?: boolean;
87-
finallyRan: boolean;
87+
// Callbacks that have already fired for this shared execution. When
88+
// .default() produces a clone, both parent and child share `exec`; each
89+
// instance's .finally() callback must still fire exactly once. Keying by
90+
// callback reference keeps the guard per-instance while the state lives
91+
// on the shared execution.
92+
finallyRan: Set<() => void | Promise<void>>;
8893
// Set of breadcrumbConfig objects whose breadcrumbs have already been
8994
// emitted for this shared execution. Shared across .default() clones so
9095
// a parent + child referencing the same config emit breadcrumbs only
@@ -151,7 +156,7 @@ export class Try<
151156
this.fn = fn;
152157
this.args = args;
153158
this.config = { tags: {} };
154-
this.exec = { state: 'pending', finallyRan: false, breadcrumbsEmitted: new Set() };
159+
this.exec = { state: 'pending', finallyRan: new Set(), breadcrumbsEmitted: new Set() };
155160
this.local = { breadcrumbsAdded: false };
156161
// Only `AsyncFunction`s are thenable: `installThenable()` defines an owned
157162
// `.then` data property so `await new Try(asyncFn)` works without
@@ -745,9 +750,21 @@ export class Try<
745750
*/
746751
private execute(): TryResult<TReturn> | Promise<TryResult<TReturn>> {
747752
if (this.exec.state === 'executed' && this.exec.result) {
748-
return this.exec.isAsync
749-
? (this.exec.promise as Promise<TryResult<TReturn>>)
750-
: this.exec.result;
753+
if (this.exec.isAsync) {
754+
// Chain this instance's finally onto the settled promise so clones
755+
// (from .default()) each run their own finallyCallback exactly once.
756+
return (this.exec.promise as Promise<TryResult<TReturn>>).then(
757+
(result) => {
758+
const ran = this.runFinallyCallback();
759+
return isPromiseLike(ran)
760+
? Promise.resolve(ran).then(() => result)
761+
: result;
762+
},
763+
);
764+
}
765+
// Sync cached path: run this instance's finally if not already run.
766+
void this.runFinallyCallback();
767+
return this.exec.result;
751768
}
752769

753770
if (this.exec.promise) {
@@ -813,13 +830,14 @@ export class Try<
813830
}
814831

815832
private runFinallyCallback(): void | Promise<void> {
816-
if (!this.config.finallyCallback || this.exec.finallyRan) {
833+
const cb = this.config.finallyCallback;
834+
if (!cb || this.exec.finallyRan.has(cb)) {
817835
return;
818836
}
819-
this.exec.finallyRan = true;
837+
this.exec.finallyRan.add(cb);
820838

821839
try {
822-
const result = this.config.finallyCallback();
840+
const result = cb();
823841
if (isPromiseLike(result)) {
824842
return Promise.resolve(result).catch((err: unknown) => {
825843
if (this.config.debug) {

0 commit comments

Comments
 (0)