Skip to content

useAsync's refresh() promise never resolves if the component unmounts before the triggered fetch settles #260

Description

@Jagadeeshftw

📌 Description

useAsync (src/hooks/useAsync.ts) implements refresh() by pushing a resolve callback onto refreshWaiters.current and bumping nonce to re-trigger the fetch effect; the effect's .finally() is supposed to drain and call all queued waiters once the fetch settles. However, that .finally() starts with if (controller.signal.aborted) return; — if the component unmounts (or reload/refresh fires again) before the in-flight fetch completes, the effect's cleanup aborts the controller, and the .finally() handler exits early WITHOUT resolving the waiters that were queued for that fetch. Any caller that did await refresh() — e.g. SettlementDetail's run() (src/components/SettlementDetail.tsx), which calls await refresh() after a successful execute/cancel — is left with a promise that never resolves if the component unmounts in that window, silently leaking a pending promise.

🧩 Requirements and context

  • refresh()'s returned promise should always eventually settle (resolve) — either when its triggered fetch completes, or, if the component/effect is torn down first (unmount, or a newer reload/refresh superseding it), resolve immediately at cleanup time rather than never.
  • The existing "resolves when the fetch settles, never rejects" contract documented on refresh should be preserved for the normal (non-aborted) case.
  • No change to reload's behavior (it doesn't return a promise and isn't affected).

🛠️ Suggested execution

  • Update the effect's cleanup function in src/hooks/useAsync.ts to drain and resolve any still-pending refreshWaiters (in addition to the abort call), so an unmount or a superseding reload/refresh doesn't strand earlier waiters.
  • Extend src/hooks/useAsync.test.ts with a test that calls refresh(), unmounts the hook before the mocked load promise resolves, and asserts the refresh() promise still settles.

✅ Acceptance criteria

  • refresh()'s returned promise always eventually resolves, even if the component unmounts before the triggered fetch settles.
  • The normal (non-aborted) resolve-after-fetch-settles behavior is unchanged.
  • Test coverage for the unmount-before-settle case.

🔒 Security notes

No new attack surface; a promise-lifecycle correctness fix preventing a dangling await.

📋 Guidelines

  • Minimum 95% test coverage
  • Clear documentation
  • Timeframe: 96 hours

Metadata

Metadata

Assignees

Labels

GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardOfficial Campaign | FWC26Campaign: Official Campaign | FWC26bugSomething isn't workingfrontendFrontend / UI work

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions