Summary
PromiseCache in @gitkraken/core-gitlens leaks an unhandledRejection from its cache-cleanup branch. When a cached factory promise rejects, the void promise.finally(...) cleanup chain in getOrCreate has no rejection handler, so the rejection surfaces as a process-level unhandledRejection. The rejecting promise is cache-internal and unreachable from the caller, so neither the caller's try/catch nor a .catch on the value returned by getOrCreate can suppress it (verified empirically).
This was found while debugging an unhandledRejection storm in Kepler (105 in one prod log). The root cause is in core-gitlens, not in Kepler.
Similar to a previously reported issue in the same area.
Where
dist/utils/promiseCache.js (source: packages/core/src/utils/promiseCache.ts), in getOrCreate.
There are two void promise.finally(...) cleanup chains that are not followed by a .catch:
PromiseCache ~line 105 / 152
PromiseMap ~line 301 / 352
void promise.finally(() => { /* cleanup: dispose aggregate, delete key */ });
When the cached factory promise rejects (e.g. a git merge-base [--fork-point] that exits 1 with empty stderr — a legitimate "no fork-point / no common ancestor" result that defaultExceptionHandler re-throws), this finally-derived promise has no rejection handler, so it surfaces as a process-level unhandledRejection.
Telling detail
The sibling caller branch attachCallerLifecycle already guards itself:
void returned.finally(cleanup).catch(() => {});
with a comment literally saying "Attach .catch to swallow any rejection in the cleanup chain so it doesn't surface as an unhandled rejection."
The cleanup branch in getOrCreate is missing that same .catch, which looks like an oversight rather than intent. Present in current main.
Repro (against 0.3.0)
Call a cached, rejecting op (e.g. refs.getMergeBase between two commits with no common ancestor) without consuming its promise, or with an AbortSignal that fires mid-flight. A 5x burst produces 10 unhandled rejections; the native stack is just Git.runCore → the cache branch in git-cli/exec/git.js.
Suggested fix
Append .catch(() => {}) to both void promise.finally(...) cleanup chains, mirroring attachCallerLifecycle.
Notes
We've shipped a temporary pnpm patch on our side (Kepler PR gitkraken/kepler#1154) pinned to 0.3.0 to stop the bleeding, but it'd be great to fix it upstream so we can drop the patch.
Summary
PromiseCachein@gitkraken/core-gitlensleaks anunhandledRejectionfrom its cache-cleanup branch. When a cached factory promise rejects, thevoid promise.finally(...)cleanup chain ingetOrCreatehas no rejection handler, so the rejection surfaces as a process-levelunhandledRejection. The rejecting promise is cache-internal and unreachable from the caller, so neither the caller'stry/catchnor a.catchon the value returned bygetOrCreatecan suppress it (verified empirically).This was found while debugging an
unhandledRejectionstorm in Kepler (105 in one prod log). The root cause is in core-gitlens, not in Kepler.Similar to a previously reported issue in the same area.
Where
dist/utils/promiseCache.js(source:packages/core/src/utils/promiseCache.ts), ingetOrCreate.There are two
void promise.finally(...)cleanup chains that are not followed by a.catch:PromiseCache~line 105 / 152PromiseMap~line 301 / 352When the cached factory promise rejects (e.g. a
git merge-base [--fork-point]that exits 1 with empty stderr — a legitimate "no fork-point / no common ancestor" result thatdefaultExceptionHandlerre-throws), thisfinally-derived promise has no rejection handler, so it surfaces as a process-levelunhandledRejection.Telling detail
The sibling caller branch
attachCallerLifecyclealready guards itself:with a comment literally saying "Attach .catch to swallow any rejection in the cleanup chain so it doesn't surface as an unhandled rejection."
The cleanup branch in
getOrCreateis missing that same.catch, which looks like an oversight rather than intent. Present in currentmain.Repro (against 0.3.0)
Call a cached, rejecting op (e.g.
refs.getMergeBasebetween two commits with no common ancestor) without consuming its promise, or with anAbortSignalthat fires mid-flight. A 5x burst produces 10 unhandled rejections; the native stack is justGit.runCore→ the cache branch ingit-cli/exec/git.js.Suggested fix
Append
.catch(() => {})to bothvoid promise.finally(...)cleanup chains, mirroringattachCallerLifecycle.Notes
We've shipped a temporary
pnpm patchon our side (Kepler PR gitkraken/kepler#1154) pinned to 0.3.0 to stop the bleeding, but it'd be great to fix it upstream so we can drop the patch.