Skip to content

Unhandled rejections from user-initiated list reloads: 17 of 18 void runWithCommandAuthRecovery(...) sites have no rejection handler #2049

Description

@cliffhall

Found while smoke-testing the v2.3.0 milestone, verifying #1998 (PR #2047).

The problem

runWithCommandAuthRecovery rethrows anything that isn't an AuthRecoveryRequiredError (clients/web/src/App.tsx:2063):

} catch (err) {
  if (err instanceof AuthRecoveryRequiredError) { /* …recover… */ }
  throw err;   // <- everything else propagates to the caller
}

It is called as void runWithCommandAuthRecovery(...) at 18 sites in App.tsx. Exactly one of them attaches a rejection handler — onRefreshTasks (:3679), which .catch()es and shows a notification. The other 17 have no handler, so any non-auth failure of the wrapped operation becomes an unhandled rejection in the browser.

Reproduced

Against a stdio server whose tools/list rejects, with the browser listening on unhandledrejection:

Action Path Result
Connect (paginated) PagedToolsState connect-time load clean — fixed by #1998
Connect (all-pages) managed store load clean
Retry (paginated) onRefreshTools:3505 unhandledrejection: ProtocolError: …
Retry (all-pages) onRefreshTools:3510 unhandledrejection: ProtocolError: …
Load next page onLoadMoreTools:3635 unhandledrejection: ProtocolError: …

Chromium also surfaces these as a pageerror. Prompts and resources have the identical wiring.

So this is not a #1998 leftover: #1998 fixed the connect-time float inside the paged states, and the remaining floats are in the user-initiated handlers, in both pagination modes. That wiring predates it — onLoadMore* came in with #1721 (b3b04c6, 2026-07-19) and is untouched by #2047.

Why it matters

Nothing is visibly broken: in every case above the list panel already renders "Couldn't load tools" with the real message and a Retry, so the user sees the failure. The cost is elsewhere:

  • Two gates that should catch this can't. void is the sanctioned suppression for @typescript-eslint/no-floating-promises, so Enable @typescript-eslint/no-floating-promises so an unhandled rejection can't fail the gate again #1959 does not flag these. And smoke:web:browser hard-fails on exactly this signal (unhandled rejection / pageerror) — it just never connects to a server, so it never reaches the code. Any future smoke that drives a failing list turns red on a bug nobody introduced.
  • This is a debugging tool. A spurious uncaught ProtocolError in the console is worse here than in an ordinary app: someone inspecting their own server sees an error the Inspector produced, not one their server did, and has to rule it out.

Suggested shape

Give every void runWithCommandAuthRecovery(...) a rejection handler. Two classes, and the file already contains an example of each:

  1. The failure is already surfaced in the panel (the list refresh / load-more paths — the paged and managed stores both record the error). A terminal .catch(() => {}) is enough, with a comment saying the error is displayed elsewhere — the same reasoning PagedToolsState uses for its connect-time void this.loadPage(undefined).catch(() => {}).
  2. Nothing else records it. Follow onRefreshTasks (:3679) and show a notification.

Worth auditing all 18 sites rather than only the three verified above, since the same void shape is used for several other commands.

A cheaper structural option, if it fits: have the handlers await through a small wrapper that owns the reporting, so a new call site cannot reintroduce the gap by omission.

Acceptance

  • With a server whose list calls reject, neither Retry nor Load next page produces an unhandledrejection or pageerror, in either pagination mode.
  • The existing error UI (alert + Retry, page 1 retained on a page-2 failure) is unchanged.
  • No void runWithCommandAuthRecovery(...) in App.tsx is left without a rejection handler.

Notes

Pre-existing; not a regression from anything in v2.3.0. Filed against v2.4.0 rather than v2.3.0, which is due 2026-08-19 with only the release bump (#2013) outstanding.

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingv2Issues and PRs for v2

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions